تعرف على Ecto ، غلاف قاعدة البيانات غير المساومة لتطبيقات الإكسير المتزامنة
نشرت: 2022-03-11Ecto هي لغة مجال محدد لكتابة الاستفسارات والتفاعل مع قواعد البيانات بلغة Elixir. يدعم الإصدار الأخير (2.0) PostgreSQL و MySQL. (سيتوفر دعم MSSQL و SQLite و MongoDB في المستقبل). إذا كنت جديدًا على Elixir أو لديك خبرة قليلة في استخدامه ، فإنني أوصيك بقراءة Kleber Virgilio Correia's Getting Started with Elixir Programming Language.
يتكون Ecto من أربعة مكونات رئيسية:
- إكتو ريبو. يحدد المستودعات التي هي أغلفة حول مخزن البيانات. باستخدامه ، يمكننا إدراج وإنشاء وحذف والاستعلام عن الريبو. مطلوب محول وبيانات اعتماد للتواصل مع قاعدة البيانات.
- Ecto.Schema. تُستخدم المخططات لتعيين أي مصدر بيانات في بنية إكسير.
- Ecto.Changeset. توفر التغييرات طريقة للمطورين لتصفية وإخراج المعلمات الخارجية ، بالإضافة إلى آلية لتتبع التغييرات والتحقق من صحتها قبل تطبيقها على البيانات.
- Ecto.Query. يوفر استعلام SQL يشبه DSL لاسترداد المعلومات من المستودع. الاستعلامات في Ecto آمنة ، وتتجنب المشاكل الشائعة مثل حقن SQL ، بينما لا تزال قابلة للتكوين ، مما يسمح للمطورين ببناء الاستعلامات قطعة تلو الأخرى بدلاً من الكل مرة واحدة.
في هذا البرنامج التعليمي ، سوف تحتاج إلى:
- تم تثبيت Elixir (دليل التثبيت لـ 1.2 أو أحدث)
- تم تثبيت PostgreSQL
- مستخدم محدد بإذن لإنشاء قاعدة بيانات (ملاحظة: سنستخدم المستخدم "postgres" بكلمة المرور "postgres" كمثال خلال هذا البرنامج التعليمي.)
التثبيت والتكوين
بالنسبة للمبتدئين ، دعنا ننشئ تطبيقًا جديدًا مع مشرف باستخدام 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 deps do
postgrex (وهو محول قاعدة البيانات) و ecto . بمجرد تحرير الملف ، قم بتشغيل في وحدة التحكم:
mix deps.get
سيؤدي هذا إلى تثبيت جميع التبعيات وإنشاء ملف mix.lock
يخزن جميع التبعيات والتبعيات الفرعية للحزم المثبتة (على غرار Gemfile.lock
في المجمع).
إكتو ريبو
سننظر الآن في كيفية تحديد الريبو في تطبيقنا. يمكن أن يكون لدينا أكثر من ريبو ، مما يعني أنه يمكننا الاتصال بأكثر من قاعدة بيانات واحدة. نحتاج إلى تكوين قاعدة البيانات في الملف 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
يقوم هذا الأمر بإنشاء قاعدة البيانات ، وبذلك نكون قد أنهينا التكوين بشكل أساسي. نحن الآن جاهزون لبدء البرمجة ، لكن دعنا نحدد نطاق تطبيقنا أولاً.
بناء فاتورة مع العناصر المضمنة
بالنسبة لتطبيقنا التجريبي ، سنقوم ببناء أداة بسيطة للفواتير. بالنسبة لمجموعات التغييرات (الطرز) ، سيكون لدينا الفاتورة ، والبند ، والبند الفاتورة . InvoiceItem ينتمي إلى الفاتورة والبند . يمثل هذا الرسم البياني كيفية ارتباط نماذجنا ببعضها البعض:
الرسم التخطيطي بسيط جدا. لدينا جدول فواتير يحتوي على العديد من عناصر_الفاتورة حيث نقوم بتخزين جميع التفاصيل وأيضًا عناصر الجدول التي تحتوي على العديد من عناصر_الفاتورة . يمكنك أن ترى أن نوع invoice_id و item_id في جدول invoice_items هو UUID. نحن نستخدم UUID لأنه يساعد في تشويش المسارات ، في حالة رغبتك في كشف التطبيق عبر واجهة برمجة تطبيقات وتجعل المزامنة أسهل لأنك لا تعتمد على رقم تسلسلي. لنقم الآن بإنشاء الجداول باستخدام مهام Mix.
الهجرة
عمليات الترحيل هي ملفات تُستخدم لتعديل مخطط قاعدة البيانات. يمنحك 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
لقد أنشأنا invoice
الجدول مع جميع الحقول المحددة. لنقم بإنشاء جدول العناصر :
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
تحرير الترحيل:
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 بقيد في قاعدة البيانات يشير إلى جدول الفواتير . لدينا نفس النمط لحقل item_id . الشيء الآخر المختلف هو طريقة إنشاء الفهرس: create index(:invoice_items, [:invoice_id])
ينشئ الفهرس invoice_items_invoice_id_index .
Ecto.Schema و Ecto.Changeset
في Ecto.Model
، تم إهمال 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
في الجزء العلوي ، نقوم بحقن كود في مجموعة التغييرات باستخدام use Ecto.Schema
. نحن نستخدم أيضًا import Ecto.Changeset
لاستيراد الوظائف من Ecto.Changeset . كان بإمكاننا تحديد الطرق المحددة للاستيراد ، ولكن دعنا نجعلها بسيطة. يسمح لنا alias 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 ( validate_required([:name, :price])
للتحقق من وجود حقلي الاسم والسعر ، validate_number(:price, greater_than_or_equal_to: Decimal.new(0))
التحقق من أن الرقم أكبر من أو يساوي 0 أو في هذه الحالة Decimal.new(0)
.
كان هذا كثيرًا لتستوعبه ، لذلك دعونا نلقي نظرة على هذا في وحدة التحكم بأمثلة حتى تتمكن من فهم المفاهيم بشكل أفضل:
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
يعرّف علاقة "ينتمي إلى" مع مجموعة التغييرات Cart.Invoice التي سننشئها قريبًا. ينتمي العنصر التالي belongs_to :item
علاقة مع جدول العناصر. لقد حددنا @zero Decimal.new(0)
. في هذه الحالة ، يعد zero بمثابة ثابت يمكن الوصول إليه داخل الوحدة النمطية. تحتوي وظيفة مجموعة التغييرات على أجزاء جديدة ، أحدها 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
سلة التسوق: هناك بعض الاختلافات في مجموعة تغييرات الصوت. الأول هو داخل المخططات : has_many :invoice_items, InvoiceItem, on_delete: :delete_all
تعني أنه عندما نحذف فاتورة ، سيتم حذف جميع عناصر_الفاتورة المرتبطة بها. ضع في اعتبارك ، مع ذلك ، أن هذا ليس قيدًا محددًا في قاعدة البيانات.
لنجرب طريقة الإنشاء في وحدة التحكم لفهم الأشياء بشكل أفضل. ربما تكون قد أنشأت العناصر ("الورق" ، "المقص") التي سنستخدمها هنا:
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)
Ecto.Query
لقد أوضحنا حتى الآن كيفية إنشاء عناصر جديدة وفواتير جديدة مع العلاقات. لكن ماذا عن الاستعلام؟ حسنًا ، اسمحوا لي أن أقدم لكم Ecto.Query الذي سيساعدنا في تقديم استفسارات إلى قاعدة البيانات ، لكن أولاً نحتاج إلى المزيد من البيانات لشرح أفضل.
iex(1)> alias Cart.{Repo, Item, Invoice, InvoiceItem} iex(2)> Repo.insert(%Item{name: "Chocolates", price: Decimal.new("5")}) iex(3)> Repo.insert(%Item{name: "Gum", price: Decimal.new("2.5")}) iex(4)> Repo.insert(%Item{name: "Milk", price: Decimal.new("1.5")}) iex(5)> Repo.insert(%Item{name: "Rice", price: Decimal.new("2")}) iex(6)> Repo.insert(%Item{name: "Chocolates", price: Decimal.new("10")})
يجب أن يكون لدينا الآن 8 عناصر وهناك "شوكولاتة" متكررة. قد نرغب في معرفة العناصر المكررة. لذلك دعونا نجرب هذا الاستعلام:
iex(7)> import Ecto.Query iex(8)> q = from(i in Item, select: %{name: i.name, count: (i.name)}, group_by: i.name) iex(9)> Repo.all(q) 19:12:15.739 [debug] QUERY OK db=2.7ms SELECT i0."name", count(i0."name") FROM "items" AS i0 GROUP BY i0."name" [] [%{count: 1, name: "Scissors"}, %{count: 1, name: "Gum"}, %{count: 2, name: "Chocolates"}, %{count: 1, name: "Paper"}, %{count: 1, name: "Milk"}, %{count: 1, name: "Test"}, %{count: 1, name: "Rice"}]
يمكنك أن ترى أنه في الاستعلام أردنا إرجاع خريطة باسم العنصر وعدد مرات ظهوره في جدول العناصر. بدلاً من ذلك ، قد نكون مهتمين على الأرجح بمعرفة المنتجات الأكثر مبيعًا. لذلك ، دعونا ننشئ بعض الفواتير. أولاً ، لنجعل حياتنا أسهل من خلال إنشاء خريطة للوصول إلى item_id
:
iex(10)> l = Repo.all(from(i in Item, select: {i.name, i.id})) iex(11)> items = for {k, v} <- l, into: %{}, do: {k, v} %{"Chocolates" => "8fde33d3-6e09-4926-baff-369b6d92013c", "Gum" => "cb1c5a93-ecbf-4e4b-8588-cc40f7d12364", "Milk" => "7f9da795-4d57-4b46-9b57-a40cd09cf67f", "Paper" => "66ab2ab7-966d-4b11-b359-019a422328d7", "Rice" => "ff0b14d2-1918-495e-9817-f3b08b3fa4a4", "Scissors" => "397b0bb4-2b04-46df-84d6-d7b1360b6c72", "Test" => "9f832a81-f477-4912-be2f-eac0ec4f8e8f"}
كما ترى قمنا بإنشاء خريطة باستخدام الفهم
iex(12)> line_items = [%{item_id: items["Chocolates"], quantity: 2}]
نحتاج إلى إضافة السعر في معلمات invoice_items
لإنشاء فاتورة ، ولكن سيكون من الأفضل فقط تمرير معرف العنصر وملء السعر تلقائيًا. سنقوم بإجراء تغييرات على وحدة Cart.Invoice لإنجاز ذلك:
defmodule Cart.Invoice do use Ecto.Schema import Ecto.Changeset import Ecto.Query # We add to query # .... # schema, changeset and create functions don't change # The new function here is items_with_prices defp get_items(params) do items = items_with_prices(params[:invoice_items] || params["invoice_items"]) Enum.map(items, fn(item)-> InvoiceItem.changeset(%InvoiceItem{}, item) end) end # new function to get item prices defp items_with_prices(items) do item_ids = Enum.map(items, fn(item) -> item[:item_id] || item["item_id"] end) q = from(i in Item, select: %{id: i.id, price: i.price}, where: i.id in ^item_ids) prices = Repo.all(q) Enum.map(items, fn(item) -> item_id = item[:item_id] || item["item_id"] %{ item_id: item_id, quantity: item[:quantity] || item["quantity"], price: Enum.find(prices, fn(p) -> p[:id] == item_id end)[:price] || 0 } end) end
أول شيء ستلاحظه هو أننا أضفنا Ecto.Query ، مما سيسمح لنا بالاستعلام عن قاعدة البيانات. الوظيفة الجديدة هي defp items_with_prices(items) do
بالبحث من خلال العناصر والعثور على وتحديد السعر لكل عنصر.
أولاً ، defp items_with_prices(items) do
قائمة كوسيطة. باستخدام item_ids = Enum.map(items, fn(item) -> item[:item_id] || item["item_id"] end)
، نكرر جميع العناصر ونحصل على item_id فقط. كما ترى ، يمكننا الوصول إما باستخدام atom :item_id
أو السلسلة "item_id" ، نظرًا لأن الخرائط يمكن أن تحتوي على أيٍّ من هذين الأمرين كمفاتيح. سيجد الاستعلام q = from(i in Item, select: %{id: i.id, price: i.price}, where: i.id in ^item_ids)
كافة العناصر الموجودة في item_ids
خريطة بها item.id
و item.price
. يمكننا بعد ذلك تشغيل prices = Repo.all(q)
الذي يعرض قائمة بالخرائط. نحتاج بعد ذلك إلى تكرار العناصر وإنشاء قائمة جديدة تضيف السعر. Enum.map(items, fn(item) ->
تتكرر خلال كل عنصر ، وتجد السعر Enum.find(prices, fn(p) -> p[:id] == item_id end)[:price] || 0
، ويقوم بإنشاء قائمة جديدة تحتوي على item_id
والكمية والسعر. وبذلك ، لم يعد من الضروري إضافة السعر في كل عنصر من invoice_items
.
إدخال المزيد من الفواتير
كما تتذكر ، قمنا سابقًا بإنشاء عناصر خريطة تمكننا من الوصول إلى المعرف باستخدام اسم العنصر items["Gum"]
"cb1c5a93-ecbf-4e4b-8588-cc40f7d12364". هذا يجعل من السهل إنشاء invoice_items . لنقم بإنشاء المزيد من الفواتير. ابدأ تشغيل وحدة التحكم مرة أخرى وقم بتشغيل:
Iex -S mix
iex(1)> Repo.delete_all(InvoiceItem); Repo.delete_all(Invoice)
نحذف جميع invoice_items والفواتير للحصول على قائمة فارغة:
iex(2)> li = [%{item_id: items["Gum"], quantity: 2}, %{item_id: items["Milk"], quantity: 1}] iex(3)> Invoice.create(%{customer: "Mary Jane", date: Ecto.Date.utc, invoice_items: li}) iex(4)> li2 = [%{item_id: items["Chocolates"], quantity: 2}| li] iex(5)> Invoice.create(%{customer: "Mary Jane", date: Ecto.Date.utc, invoice_items: li2}) iex(5)> li3 = li2 ++ [%{item_id: items["Paper"], quantity: 3 }, %{item_id: items["Rice"], quantity: 1}, %{item_id: items["Scissors"], quantity: 1}] iex(6)> Invoice.create(%{customer: "Juan Perez", date: Ecto.Date.utc, invoice_items: li3})
الآن لدينا 3 فواتير ؛ الأولى تحتوي على عنصرين ، والثانية بها 3 عناصر ، والثالثة بها 6 عناصر. نود الآن أن نعرف ما هي المنتجات الأكثر مبيعًا؟ للإجابة على ذلك ، سننشئ استعلامًا للعثور على العناصر الأكثر مبيعًا حسب الكمية والإجمالي الفرعي (السعر × الكمية).
defmodule Cart.Item do use Ecto.Schema import Ecto.Changeset import Ecto.Query alias Cart.{InvoiceItem, Item, Repo} # schema and changeset don't change # ... def items_by_quantity, do: Repo.all items_by(:quantity) def items_by_subtotal, do: Repo.all items_by(:subtotal) defp items_by(type) do from i in Item, join: ii in InvoiceItem, on: ii.item_id == i.id, select: %{id: i.id, name: i.name, total: sum(field(ii, ^type))}, group_by: i.id, order_by: [desc: sum(field(ii, ^type))] end end
نقوم باستيراد Ecto.Query ثم نسمي alias Cart.{InvoiceItem, Item, Repo}
لذلك لا نحتاج إلى إضافة سلة التسوق في بداية كل وحدة. تستدعي الوظيفة الأولى items_by_quantity الدالة items_by
، وتمرير معلمة :quantity
واستدعاء Repo.all لتنفيذ الاستعلام. تتشابه الوظيفة items_by_subtotal مع الوظيفة السابقة ولكنها تمرر المعلمة :subtotal
sub total. الآن دعنا نشرح items_by :
-
from i in Item
، يحدد هذا الماكرو وحدة العنصر -
join: ii in InvoiceItem, on: ii.item_id == i.id
، يُنشئ صلة بالشرط "items.id = invoice_items.item_id" -
select: %{id: i.id, name: i.name, total: sum(field(ii, ^type))}
، نقوم بإنشاء خريطة بكل الحقول التي نريدها أولاً نختار المعرف والاسم من العنصر ونقوم بحساب مجموع عامل التشغيل. يستخدم الحقل (ii ، ^ type) حقل الماكرو للوصول ديناميكيًا إلى أحد الحقول -
group_by: i.id
، نقوم بالتجميع حسب العناصر -
order_by: [desc: sum(field(ii, ^type))]
وأخيراً الترتيب حسب المجموع بترتيب تنازلي
لقد كتبنا حتى الآن الاستعلام في نمط القائمة ولكن يمكننا إعادة كتابته بأسلوب ماكرو:
defp items_by(type) do Item |> join(:inner, [i], ii in InvoiceItem, ii.item_id == i.id) |> select([i, ii], %{id: i.id, name: i.name, total: sum(field(ii, ^type))}) |> group_by([i, _], i.id) |> order_by([_, ii], [desc: sum(field(ii, ^type))]) end
أفضل كتابة الاستفسارات في شكل قائمة لأنني أجدها أكثر قابلية للقراءة.
خاتمة
لقد غطينا جزءًا كبيرًا مما يمكنك القيام به في تطبيق باستخدام Ecto. بالطبع ، هناك الكثير الذي يمكنك تعلمه من مستندات Ecto. باستخدام Ecto ، يمكنك إنشاء تطبيقات متزامنة ومتسامحة مع الأخطاء بأقل جهد يمكن توسيعها بسهولة بفضل الجهاز الظاهري Erlang. يوفر Ecto الأساس للتخزين في تطبيقات Elixir ويوفر وظائف ووحدات ماكرو لإدارة بياناتك بسهولة.
في هذا البرنامج التعليمي ، قمنا بفحص Ecto.Schema و Ecto.Changeset و Ecto.Migration و Ecto.Query و Ecto.Repo. تساعدك كل من هذه الوحدات في أجزاء مختلفة من تطبيقك وتجعل الكود أكثر وضوحًا ويسهل صيانته وفهمه.
إذا كنت تريد التحقق من رمز البرنامج التعليمي ، فيمكنك العثور عليه هنا على GitHub.
إذا أعجبك هذا البرنامج التعليمي وكنت مهتمًا بمزيد من المعلومات ، فإنني أوصي بـ Phoenix (للحصول على قائمة بالمشاريع الرائعة) ، و Awesome Elixir ، وهذا الحديث الذي يقارن ActiveRecord مع Ecto.