يجعل Elixir ببنائه البسيط والنظيف بناء تطبيقات متزامنة قابلة للتطوير والصيانة أمرًا سهلاً. Ecto عبارة عن غلاف قاعدة بيانات يرقى إلى مستوى التوقعات العالية التي حددتها سمعة Elixir. توفر لغتها الخاصة بالمجال طريقة ممتعة للتفاعل مع قواعد البيانات وإنشاء تطبيقات متزامنة تتسامح مع الأخطاء في Elixir بسهولة. في هذه المقالة يرشدنا مهندس برمجيات Toptal المستقل بوريس باروسو عبر Ecto ومكوناته الأربعة الرئيسية: Repo و Schema و Changeset و Query.
Ecto هي لغة خاصة بالمجال لكتابة الاستفسارات والتفاعل مع قواعد البيانات بلغة Elixir. يدعم الإصدار الأخير (2.0) PostgreSQL و MySQL.( سيتوفر دعم MSSQL و SQLite و MongoDB في المستقبل).

هل سئمت من كل لهجات SQL؟ تحدث إلى قاعدة البيانات الخاصة بك من خلال Ecto.
مكونات Ecto
يتكون Ecto من أربعة مكونات رئيسية:
- Ecto.repo. يحدد المستودعات التي هي أغلفة حول مخزن البيانات. باستخدامه يمكننا إدراج وإنشاء وحذف والاستعلام عن الريبو. مطلوب محول وبيانات اعتماد للتواصل مع قاعدة البيانات.
- Ecto.Schema. تُستخدم المخططات لتعيين أي مصدر بيانات في بنية إكسير.
- Ecto.Changeset. توفر التغييرات طريقة للمطورين لتصفية وإخراج المعلمات الخارجية بالإضافة إلى آلية لتتبع التغييرات والتحقق من صحتها قبل تطبيقها على البيانات.
- Ecto.Query. يوفر استعلام SQL يشبه DSL لاسترداد المعلومات من المستودع. الاستعلامات في Ecto آمنة وتتجنب المشاكل الشائعة مثل حقن SQL بينما لا تزال قابلة للتكوين مما يسمح للمطورين ببناء الاستعلامات قطعة تلو الأخرى بدلاً من الكل مرة واحدة.
في هذا البرنامج التعليمي سوف تحتاج إلى:
- تثبيت Elixir (دليل التثبيت لـ 1.2 أو أحدث)
- تثبيت PostgreSQL
- مستخدم محدد بإذن لإنشاء قاعدة بيانات (ملاحظة: سنستخدم المستخدم “postgres” بكلمة المرور “postgres” كمثال خلال هذا البرنامج التعليمي.)
تثبيت وتكوين Ecto
بالنسبة للمبتدئين دعنا ننشئ تطبيقًا جديدًا مع مشرف باستخدام Mix. Mix هي أداة بناء تأتي مع Elixir والتي توفر مهام لإنشاء وتجميع واختبار تطبيقك وإدارة تبعياته وغير ذلك الكثير.
mix new cart --sup
سيؤدي هذا إلى إنشاء عربة دليل بملفات المشروع الأولية:
* creating README.md * creating .gitignore * creating mix.exs * creating config * creating config/config.exs * creating lib * creating lib/ecto_tut.ex * creating test * creating test/test_helper.exs * creating test/ecto_tut_test.exs
نحن نستخدم الخيار – sup لأننا نحتاج إلى شجرة مشرف تحافظ على الاتصال بقاعدة البيانات. بعد ذلك نذهب إلى دليل cart
باستخدام cd cart ونفتح ملف mix.exs ونستبدل محتوياته:
defmodule Cart.Mixfile do use Mix.Project def project do [app: :cart, version: "0.0.1", elixir: "~> 1.2", build_embedded: Mix.env == :prod, start_permanent: Mix.env == :prod, deps: deps] end def application do [applications: [:logger, :ecto, :postgrex], mod: {Cart, []}] end # Type "mix help deps" for more examples and options defp deps do [{:postgrex, ">= 0.11.1"}, {:ecto, "~> 2.0"}] end end
في def application do
يتعين علينا إضافة تطبيقات: :postgrex, :ecto
بحيث يمكن استخدامها داخل تطبيقنا. يتعين علينا أيضًا إضافة هذه على أنها تبعيات عن طريق إضافة أقسام defp do postgrex (وهو محول قاعدة البيانات) و ecto. بمجرد تحرير الملف قم بتشغيل في وحدة التحكم:
mix deps.get
سيؤدي هذا إلى تثبيت جميع التبعيات وإنشاء ملف mix.lock يخزن جميع التبعيات والتبعيات الفرعية للحزم المثبتة (على غرار Gemfile.lock في المجمع).
إكتو ريبو (Ecto.Repo)

سننظر الآن في كيفية تحديد الريبو في تطبيقنا. يمكن أن يكون لدينا أكثر من ريبو مما يعني أنه يمكننا الاتصال بأكثر من قاعدة بيانات واحدة. نحتاج إلى تكوين قاعدة البيانات في الملف config/config.exs
:
use Mix.Config config :cart, ecto_repos: [Cart.Repo]
نحن فقط نضع الحد الأدنى حتى نتمكن من تشغيل الأمر التالي. مع السطر: :cart, cart_repos: [Cart.Repo]
نخبر Ecto عن المستودعات التي نستخدمها. هذه ميزة رائعة لأنها تتيح لنا الحصول على العديد من عمليات إعادة الشراء أي يمكننا الاتصال بقواعد بيانات متعددة.
الآن قم بتشغيل الأمر التالي:
mix ecto.gen.repo ==> connection Compiling 1 file (.ex) Generated connection app ==> poolboy (compile) Compiled src/poolboy_worker.erl Compiled src/poolboy_sup.erl Compiled src/poolboy.erl ==> decimal Compiling 1 file (.ex) Generated decimal app ==> db_connection Compiling 23 files (.ex) Generated db_connection app ==> postgrex Compiling 43 files (.ex) Generated postgrex app ==> ecto Compiling 68 files (.ex) Generated ecto app ==> cart * creating lib/cart * creating lib/cart/repo.ex * updating config/config.exs Don't forget to add your new repo to your supervision tree (typically in lib/cart.ex): supervisor(Cart.Repo, []) And to add it to the list of ecto repositories in your configuration files (so Ecto tasks work as expected): config :cart, ecto_repos: [Cart.Repo]
هذا الأمر يولد الريبو. إذا قرأت الإخراج فسيخبرك بإضافة مشرف وإعادة الشراء في تطبيقك. لنبدأ مع المشرف. سنقوم بتعديل lib/cart.ex
defmodule Cart do use Application def start(_type, _args) do import Supervisor.Spec, warn: false children = [ supervisor(Cart.Repo, []) ] opts = [strategy: :one_for_one, name: Cart.Supervisor] Supervisor.start_link(children, opts) end end
في هذا الملف نحدد المشرف supervisor(Cart.Repo, [])
ونضيفه إلى قائمة الأطفال (في الإكسير القوائم تشبه المصفوفات). نحدد الأطفال الخاضعين للإشراف باستراتيجية strategy: :one_for_one
مما يعني أنه في حالة فشل إحدى العمليات الخاضعة للإشراف سيقوم المشرف بإعادة تشغيل هذه العملية فقط إلى حالتها الافتراضية. يمكنك معرفة المزيد عن المشرفين هنا. إذا نظرت إلى lib/cart/repo.ex
فسترى أن هذا الملف قد تم إنشاؤه بالفعل مما يعني أن لدينا ريبو لتطبيقنا.
defmodule Cart.Repo do use Ecto.Repo, otp_app: :cart end
الآن دعنا نعدل ملف التكوين config / config.exs:
use Mix.Config config :cart, ecto_repos: [Cart.Repo] config :cart, Cart.Repo, adapter: Ecto.Adapters.Postgres, database: "cart_dev", username: "postgres", password: "postgres", hostname: "localhost"
بعد تحديد جميع إعدادات قاعدة البيانات الخاصة بنا يمكننا الآن إنشاؤها عن طريق تشغيل:
mix ecto.create
يقوم هذا الأمر بإنشاء قاعدة البيانات وبذلك نكون قد أنهينا التكوين بشكل أساسي. نحن الآن جاهزون لبدء الترميز ولكن دعنا نحدد نطاق تطبيقنا أولاً.
بناء فاتورة (invoice) بالعناصر المضمنة
بالنسبة لتطبيقنا التجريبي سنقوم ببناء أداة بسيطة للفواتير. بالنسبة لمجموعات التغييرات (النماذج) سيكون لدينا invoice item invoiceItem. InvoiceItem ينتمي إلى الفاتورة والبند. يمثل هذا الرسم البياني كيف سترتبط نماذجنا ببعضها البعض:

الرسم التخطيطي بسيط جدا. لدينا جدول فواتير يحتوي على العديد من invoice_items حيث نقوم بتخزين جميع التفاصيل وأيضًا عناصر الجدول التي تحتوي على العديد من invoice_items. يمكنك أن ترى أن نوع invoice_id و item_id في جدول invoice_items هو UUID. نحن نستخدم UUID لأنه يساعد في تشويش المسارات في حالة رغبتك في عرض التطبيق عبر واجهة برمجة تطبيقات وتجعل المزامنة أسهل نظرًا لأنك لا تعتمد على رقم تسلسلي. لنقم الآن بإنشاء الجداول باستخدام مهام Mix.
Ecto.migration

عمليات الترحيل هي ملفات تُستخدم لتعديل مخطط قاعدة البيانات. يمنحك Ecto.Migration مجموعة من الأساليب لإنشاء الجداول وإضافة الفهارس وإنشاء القيود والأشياء الأخرى المتعلقة بالمخطط. تساعد عمليات الترحيل حقًا في إبقاء التطبيق متزامنًا مع قاعدة البيانات. لنقم بإنشاء برنامج نصي للترحيل لجدولنا الأول:
mix ecto.gen.migration create_invoices
سيؤدي هذا إلى إنشاء ملف مشابه لـ priv/repo/migrations/20160614115844_create_invoices.exs
حيث سنقوم بتعريف الترحيل الخاص بنا. افتح الملف الذي تم إنشاؤه وقم بتعديل محتوياته لتكون على النحو التالي:
defmodule Cart.Repo.Migrations.CreateInvoices do use Ecto.Migration def change do create table(:invoices, primary_key: false) do add :id, :uuid, primary_key: true add :customer, :text add :amount, :decimal, precision: 12, scale: 2 add :balance, :decimal, precision: 12, scale: 2 add :date, :date timestamps end end end
def change do
في الدالة الداخلية هل نحدد المخطط الذي سينشئ SQL لقاعدة البياناتcreate table(:invoices, primary_key: false) do
سوف يقوم بإنشاء جدول فواتير. لقد قمنا بتعيين Primary_key: false ولكننا سنضيف حقل معرف من نوع UUID حقل العميل من نوع النص حقل التاريخ لنوع التاريخ. ستنشئ داله timestamps
الحقلين inserted_at و updated_at اللذين يملأهما Ecto تلقائيًا بالوقت الذي تم فيه إدراج السجل ووقت تحديثه على التوالي. انتقل الآن إلى وحدة التحكم وقم بتشغيل الترحيل:
mix ecto.migrate
لقد أنشأنا invoices الجدول مع جميع الحقول المحددة. دعونا ننشئ جدول العناصر:
mix ecto.gen.migration create_items
الآن قم بتحرير نص الترحيل الذي تم إنشاؤه:
defmodule Cart.Repo.Migrations.CreateItems do use Ecto.Migration def change do create table(:items, primary_key: false) do add :id, :uuid, primary_key: true add :name, :text add :price, :decimal, precision: 12, scale: 2 timestamps end end end
الشيء الجديد هنا هو الحقل العشري الذي يسمح بالأرقام المكونة من 12 رقمًا 2 منها للجزء العشري من الرقم. لنبدأ الترحيل مرة أخرى:
mix ecto.migrate
لقد أنشأنا الآن جدول العناصر وأخيراً دعنا ننشئ جدول invoice_items:
mix ecto.gen.migration create_invoice_items
تحرير migration:
defmodule Cart.Repo.Migrations.CreateInvoiceItems do use Ecto.Migration def change do create table(:invoice_items, primary_key: false) do add :id, :uuid, primary_key: true add :invoice_id, references(:invoices, type: :uuid, null: false) add :item_id, references(:items, type: :uuid, null: false) add :price, :decimal, precision: 12, scale: 2 add :quantity, :decimal, precision: 12, scale: 2 add :subtotal, :decimal, precision: 12, scale: 2 timestamps end create index(:invoice_items, [:invoice_id]) create index(:invoice_items, [:item_id]) end end
كما ترى يحتوي هذا الترحيل على بعض الأجزاء الجديدة. أول شيء ستلاحظه هو add :invoice_id, references(:invoices, type: :uuid, null: false)
. يؤدي هذا إلى إنشاء حقل invoice_id بقيد في قاعدة البيانات يشير إلى جدول invoices. لدينا نفس النمط لحقل item_id. الشيء الآخر المختلف هو طريقة إنشاء الفهرس: create index(:invoice_items, [:invoice_id])
ينشئ الفهرس invoice_items_invoice_id_index.
Ecto.Schema و Ecto.changeset
في Ecto تم إهمال Ecto.Model لصالح استخدام Ecto.Schema لذلك سنسمي مخططات الوحدات بدلاً من النماذج. فلنقم بإنشاء مجموعات التغييرات. سنبدأ بأبسط عنصر من مجموعة التغييرات وننشئ الملف lib/cart/item.ex
defmodule Cart.Item do use Ecto.Schema import Ecto.Changeset alias Cart.InvoiceItem @primary_key {:id, :binary_id, autogenerate: true} schema "items" do field :name, :string field :price, :decimal, precision: 12, scale: 2 has_many :invoice_items, InvoiceItem timestamps end @fields ~w(name price) def changeset(data, params \\ %{}) do data |> cast(params, @fields) |> validate_required([:name, :price]) |> validate_number(:price, greater_than_or_equal_to: Decimal.new(0)) end end
في الجزء العلوي نقوم بحقن كود في مجموعة التغييرات باستخدام Ecto.Schema. نحن نستخدم أيضًا استيراد Ecto.Changeset لاستيراد الوظائف من Ecto.Changeset. كان بإمكاننا تحديد الطرق المحددة للاستيراد ولكن دعونا نجعلها بسيطة. يسمح لنا الاسم المستعار Cart.InvoiceItem بالكتابة مباشرة داخل مجموعة التغييرات InvoiceItem كما سترى بعد قليل.
Ecto.Schema

يحدد @primary_key {:id, :binary_id, autogenerate: true}
أن مفتاحنا الأساسي سيتم إنشاؤه تلقائيًا. نظرًا لأننا نستخدم نوع UUID فإننا نحدد المخطط باستخدام schema "items" do
وداخل الكتلة نحدد كل حقل وعلاقات. لقد عرّفنا الاسم على أنه سلسلة والسعر على أنه رقم عشري ، وهو مشابه جدًا للترحيل. بعد ذلك ، فإن الماكرو has_many :invoice_items, InvoiceItem
يشير إلى علاقة بين Item و InvoiceItem. نظرًا لأننا قمنا بتسمية الحقل item_id في جدول invoice_items فنحن لسنا بحاجة إلى تكوين المفتاح الخارجي. أخيرًا ستقوم طريقة الطوابع الزمنية بتعيين الحقلين inserted_at و updated_at.
Ecto.Changeset

تستقبل داله def changeset(data, params \\ %{}) do
بنية Elixir مع المعلمات التي سنمر بها عبر دوال مختلفة. يلقي cast(params, @fields)
القيم في النوع الصحيح. على سبيل المثال يمكنك تمرير السلاسل فقط في المعلمات وسيتم تحويلها إلى النوع الصحيح المحدد في المخطط. validate_required([:name, :price])
للتحقق من وجود حقلي الاسم والسعر
validate_number(:price, greater_than_or_equal_to: Decimal.new(0))
يتحقق من أن الرقم أكبر من أو يساوي 0 أو في هذه الحالة Decimal.new(0)
.
في Elixir تتم العمليات العشرية بشكل مختلف حيث يتم تنفيذها كبنية.
كان هذا كثيرًا لتستوعبه لذلك دعونا نلقي نظرة على هذا في وحدة التحكم بأمثلة حتى تتمكن من فهم المفاهيم بشكل أفضل:
iex -S mix
سيؤدي هذا إلى تحميل وحدة التحكم. -S mix يقوم بتحميل المشروع الحالي في iex REPL.
iex(0)> item = Cart.Item.changeset(%Cart.Item{}, %{name: "Paper", price: "2.5"}) #Ecto.Changeset<action: nil, changes: %{name: "Paper", price: #Decimal<2.5>}, errors: [], data: #Cart.Item<>, valid?: true>
يؤدي هذا إلى إرجاع بنية Ecto.Changeset صالحة بدون أخطاء. الآن دعونا نحفظه:
iex(1)> item = Cart.Repo.insert!(item) %Cart.Item{__meta__: #Ecto.Schema.Metadata<:loaded, "items">, id: "66ab2ab7-966d-4b11-b359-019a422328d7", inserted_at: #Ecto.DateTime<2016-06-18 16:54:54>, invoice_items: #Ecto.Association.NotLoaded<association :invoice_items is not loaded>, name: "Paper", price: #Decimal<2.5>, updated_at: #Ecto.DateTime<2016-06-18 16:54:54>}
لا نعرض SQL للإيجاز. في هذه الحالة تقوم بإرجاع Cart.Item Structure مع تعيين جميع القيم ويمكنك أن ترى أن inserted_at و updated_at يحتويان على الطوابع الزمنية الخاصة بهما وأن حقل المعرف يحتوي على قيمة UUID. دعونا نرى بعض الحالات الأخرى:
iex(3)> item2 = Cart.Item.changeset(%Cart.Item{price: Decimal.new(20)}, %{name: "Scissors"}) #Ecto.Changeset<action: nil, changes: %{name: "Scissors"}, errors: [], data: #Cart.Item<>, valid?: true> iex(4)> Cart.Repo.insert(item2)
الآن قمنا بتعيين عنصر Scissors
بطريقة مختلفة وتحديد السعر مباشرة %Cart.Item{price: Decimal.new(20)}
.
نحتاج إلى تعيين نوعه الصحيح على عكس العنصر الأول الذي مررنا فيه للتو سلسلة على أنها سعر. كان من الممكن أن نجتاز عددًا عشريًا وكان من الممكن تحويله إلى نوع عشري. إذا مررنا على سبيل المثال %Cart.Item{price: 12.5}
عند إدراج العنصر فسيتم طرح استثناء يفيد بأن النوع غير مطابق.
iex(4)> invalid_item = Cart.Item.changeset(%Cart.Item{}, %{name: "Scissors", price: -1.5}) #Ecto.Changeset<action: nil, changes: %{name: "Scissors", price: #Decimal<-1.5>}, errors: [price: {"must be greater than or equal to %{number}", [number: #Decimal<0>]}], data: #Cart.Item<>, valid?: false>
لإنهاء وحدة التحكم اضغط على Ctrl + C مرتين. يمكنك أن ترى أن عمليات التحقق من الصحة تعمل وأن السعر يجب أن يكون أكبر من أو يساوي الصفر (0). كما ترون لقد حددنا كل مخطط Ecto.Schema وهو الجزء المتعلق بكيفية تعريف بنية الوحدة وتغيير مجموعة Ecto.Changeset وهي جميع عمليات التحقق من الصحة والإرسال. دعنا نتابع وننشئ الملف lib/cart/invoice_item.ex
:
defmodule Cart.InvoiceItem do use Ecto.Schema import Ecto.Changeset @primary_key {:id, :binary_id, autogenerate: true} schema "invoice_items" do belongs_to :invoice, Cart.Invoice, type: :binary_id belongs_to :item, Cart.Item, type: :binary_id field :quantity, :decimal, precision: 12, scale: 2 field :price, :decimal, precision: 12, scale: 2 field :subtotal, :decimal, precision: 12, scale: 2 timestamps end @fields ~w(item_id price quantity) @zero Decimal.new(0) def changeset(data, params \\ %{}) do data |> cast(params, @fields) |> validate_required([:item_id, :price, :quantity]) |> validate_number(:price, greater_than_or_equal_to: @zero) |> validate_number(:quantity, greater_than_or_equal_to: @zero) |> foreign_key_constraint(:invoice_id, message: "Select a valid invoice") |> foreign_key_constraint(:item_id, message: "Select a valid item") |> set_subtotal end def set_subtotal(cs) do case {(cs.changes[:price] || cs.data.price), (cs.changes[:quantity] || cs.data.quantity)} do {_price, nil} -> cs {nil, _quantity} -> cs {price, quantity} -> put_change(cs, :subtotal, Decimal.mult(price, quantity)) end end end
مجموعة التغييرات هذه أكبر ولكن يجب أن تكون على دراية بمعظمها بالفعل. belongs_to :invoice, Cart.Invoice, type: :binary_id
يعرّف علاقة “belongs to” مع مجموعة التغييرات Cart.Invoice التي سننشئها قريبًا. ينتمي العنصر التالي إلى: ينشئ العنصر علاقة مع جدول العناصر. لقد حددناzero Decimal.new (0). في هذه الحالة يعد zero بمثابة ثابت يمكن الوصول إليه داخل الوحدة النمطية. تحتوي داله changeset على أجزاء جديدة أحدها foreign_key_constraint(:invoice_id, message: "Select a valid invoice")
.
سيسمح هذا بإنشاء رسالة خطأ بدلاً من إنشاء استثناء عندما لا يتم استيفاء القيد. وأخيرًا ستحسب الداله set_subtotal المجموع الفرعي. نجتاز مجموعة التغييرات ونعيد مجموعة تغييرات جديدة مع حساب الإجمالي الفرعي إذا كان لدينا السعر والكمية.
الآن لنقم بإنشاء Cart.Invoice. لذلك قم بإنشاء وتعديل الملف lib / cart / invoice.ex ليحتوي على ما يلي:
defmodule Cart.Invoice do use Ecto.Schema import Ecto.Changeset alias Cart.{Invoice, InvoiceItem, Repo} @primary_key {:id, :binary_id, autogenerate: true} schema "invoices" do field :customer, :string field :amount, :decimal, precision: 12, scale: 2 field :balance, :decimal, precision: 12, scale: 2 field :date, Ecto.Date has_many :invoice_items, InvoiceItem, on_delete: :delete_all timestamps end @fields ~w(customer amount balance date) def changeset(data, params \\ %{}) do data |> cast(params, @fields) |> validate_required([:customer, :date]) end def create(params) do cs = changeset(%Invoice{}, params) |> validate_item_count(params) |> put_assoc(:invoice_items, get_items(params)) if cs.valid? do Repo.insert(cs) else cs end end defp get_items(params) do items = params[:invoice_items] || params["invoice_items"] Enum.map(items, fn(item)-> InvoiceItem.changeset(%InvoiceItem{}, item) end) end defp validate_item_count(cs, params) do items = params[:invoice_items] || params["invoice_items"] if Enum.count(items) <= 0 do add_error(cs, :invoice_items, "Invalid number of items") else cs end end end
هناك بعض الاختلافات في مجموعة تغييرات cart.invoice. الأول داخل المخططات: has_many :invoice_items, InvoiceItem, on_delete: :delete_all
تعني أنه عندما نحذف فاتورة سيتم حذف جميع عناصر_الفاتورة المرتبطة بها. ضع في اعتبارك مع ذلك أن هذا ليس قيدًا محددًا في قاعدة البيانات.
دعونا نجرب طريقة الإنشاء في وحدة التحكم لفهم الأشياء بشكل أفضل. ربما تكون قد أنشأت العناصر (“paper” ، “scissors”) التي سنستخدمها هنا:
iex(0)> item_ids = Enum.map(Cart.Repo.all(Cart.Item), fn(item)-> item.id end) iex(1)> {id1, id2} = {Enum.at(item_ids, 0), Enum.at(item_ids, 1) }
لقد جلبنا جميع العناصر باستخدام Cart.Repo.all وباستخدام وظيفة Enum.map نحصل فقط على item.id لكل عنصر. في السطر الثاني نقوم فقط بتعيين id1 و id2 مع العنصرين الأول والثاني على التوالي:
iex(2)> inv_items = [%{item_id: id1, price: 2.5, quantity: 2}, %{item_id: id2, price: 20, quantity: 1}] iex(3)> {:ok, inv} = Cart.Invoice.create(%{customer: "James Brown", date: Ecto.Date.utc, invoice_items: inv_items})
تم إنشاء الفاتورة مع invoice_items الخاصة بها ويمكننا جلب جميع الفواتير الآن.
iex(4)> alias Cart.{Repo, Invoice} iex(5)> Repo.all(Invoice)
يمكنك أن ترى أنها تعيد الفاتورة ولكننا نرغب أيضًا في رؤية invoice_items:
iex(6)> Repo.all(Invoice) |> Repo.preload(:invoice_items)
باستخدام وظيفة Repo.preload يمكننا الحصول على invoice_items. لاحظ أن هذا يمكن أن يعالج الاستعلامات بشكل متزامن. في حالتي بدا الاستعلام كما يلي:
iex(7)> Repo.get(Invoice, "5d573153-b3d6-46bc-a2c0-6681102dd3ab") |> Repo.preload(:invoice_items)
This article is useful for me
1+ 2 People like this post