Faceți cunoștință cu Ecto, Wrapper pentru baze de date fără compromisuri pentru aplicațiile Elixir simultane

Publicat: 2022-03-11

Ecto este un limbaj specific domeniului pentru scrierea de interogări și interacțiunea cu bazele de date în limbajul Elixir. Cea mai recentă versiune (2.0) acceptă PostgreSQL și MySQL. (suportul pentru MSSQL, SQLite și MongoDB va fi disponibil în viitor). În cazul în care sunteți nou în Elixir sau aveți puțină experiență cu el, v-aș recomanda să citiți Noțiunile introduse în limbajul de programare Elixir a lui Kleber Virgilio Correia.

Te-ai săturat de toate dialectele SQL? Vorbiți cu baza de date prin Ecto.
Tweet

Ecto este compus din patru componente principale:

  • Ecto.Repo. Definește depozitele care sunt pachete în jurul unui depozit de date. Folosind-o, putem insera, crea, șterge și interoga un depozit. Pentru a comunica cu baza de date sunt necesare un adaptor și acreditări.
  • Ecto.Schema. Schemele sunt folosite pentru a mapa orice sursă de date într-o structură Elixir.
  • Ecto.Changeset. Seturile de modificări oferă dezvoltatorilor o modalitate de a filtra și proiecta parametrii externi, precum și un mecanism de urmărire și validare a modificărilor înainte ca acestea să fie aplicate datelor.
  • Ecto.Interogare. Oferă o interogare SQL asemănătoare DSL pentru preluarea informațiilor dintr-un depozit. Interogările din Ecto sunt sigure, evitând problemele comune precum SQL Injection, fiind în același timp compuse, permițând dezvoltatorilor să construiască interogări bucată cu bucată în loc de toate odată.

Pentru acest tutorial, veți avea nevoie de:

  • Elixir instalat (ghid de instalare pentru 1.2 sau o versiune ulterioară)
  • PostgreSQL instalat
  • Un utilizator definit cu permisiunea de a crea o bază de date (Notă: vom folosi utilizatorul „postgres” cu parola „postgres” ca exemplu în acest tutorial.)

Instalare și configurare

Pentru început, să creăm o nouă aplicație cu un supervizor folosind Mix. Mix este un instrument de compilare care este livrat cu Elixir, care oferă sarcini pentru crearea, compilarea, testarea aplicației dvs., gestionarea dependențelor acesteia și multe altele.

 mix new cart --sup

Aceasta va crea un cărucior de director cu fișierele de proiect inițiale:

 * 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

Folosim opțiunea --sup , deoarece avem nevoie de un arbore de supraveghere care va păstra conexiunea la baza de date. Apoi, mergem la directorul cart cu cd cart și deschidem fișierul mix.exs și înlocuim conținutul acestuia:

 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

În def application do trebuie să adăugăm ca aplicații :postgrex, :ecto astfel încât acestea să poată fi folosite în aplicația noastră. De asemenea, trebuie să le adăugăm ca dependențe adăugând în defp deps do postgrex (care este adaptorul bazei de date) și ecto . După ce ați editat fișierul, rulați în consolă:

 mix deps.get

Acest lucru va instala toate dependențele și va crea un fișier mix.lock care stochează toate dependențele și subdependențele pachetelor instalate (similar cu Gemfile.lock din bundler).

Ecto.Repo

Acum ne vom uita la cum să definim un repo în aplicația noastră. Putem avea mai multe repo, adică ne putem conecta la mai multe baze de date. Trebuie să configuram baza de date în fișierul config/config.exs :

 use Mix.Config config :cart, ecto_repos: [Cart.Repo]

Doar setăm minimul, astfel încât să putem rula următoarea comandă. Cu linia :cart, cart_repos: [Cart.Repo] îi spunem lui Ecto ce repoziții folosim. Aceasta este o caracteristică grozavă, deoarece ne permite să avem multe repoziții, adică ne putem conecta la mai multe baze de date.

Acum rulați următoarea comandă:

 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]

Această comandă generează repo. Dacă citiți rezultatul, vă spune să adăugați un supervizor și un repo în aplicația dvs. Să începem cu supervizorul. Vom edita 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

În acest fișier, definim supervizorul supervisor(Cart.Repo, []) și îl adăugăm la lista de copii (în Elixir, listele sunt similare cu matrice). Definim copiii supravegheați cu strategia strategy: :one_for_one ceea ce înseamnă că, dacă unul dintre procesele supravegheate eșuează, supervizorul va reporni doar acel proces în starea sa implicită. Puteți afla mai multe despre supraveghetori aici. Dacă te uiți la lib/cart/repo.ex , vei vedea că acest fișier a fost deja creat, ceea ce înseamnă că avem un Repo pentru aplicația noastră.

 defmodule Cart.Repo do use Ecto.Repo, otp_app: :cart end

Acum să edităm fișierul de configurare 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"

După ce am definit toată configurația pentru baza noastră de date, o putem genera acum rulând:

 mix ecto.create

Această comandă creează baza de date și, cu aceasta, în esență am terminat configurarea. Acum suntem gata să începem codificarea, dar să definim mai întâi domeniul de aplicare al aplicației noastre.

Crearea unei facturi cu articole inline

Pentru aplicația noastră demo, vom construi un instrument simplu de facturare. Pentru seturi de modificări (modele) vom avea Invoice , Item și InvoiceItem . InvoiceItem aparține Facturii și articolului . Această diagramă reprezintă modul în care modelele noastre vor fi legate între ele:

Diagrama este destul de simplă. Avem un tabel de facturi care are multe invoice_items unde stocăm toate detaliile și, de asemenea, un tabel de articole care are multe invoice_items . Puteți vedea că tipul pentru invoice_id și item_id din tabelul invoice_items este UUID. Folosim UUID pentru că ajută la înfundarea rutelor, în cazul în care doriți să expuneți aplicația printr-un API și o simplifică sincronizarea, deoarece nu depindeți de un număr secvenţial. Acum să creăm tabelele folosind sarcini Mix.

Ecto.Migraţie

Migrațiile sunt fișiere care sunt utilizate pentru a modifica schema bazei de date. Ecto.Migration vă oferă un set de metode pentru a crea tabele, adăuga indecși, crea constrângeri și alte chestii legate de schemă. Migrațiile ajută cu adevărat să mențină aplicația în sincronizare cu baza de date. Să creăm un script de migrare pentru primul nostru tabel:

 mix ecto.gen.migration create_invoices

Acest lucru va genera un fișier similar cu priv/repo/migrations/20160614115844_create_invoices.exs unde vom defini migrarea noastră. Deschideți fișierul generat și modificați conținutul acestuia astfel încât să fie după cum urmează:

 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

În interiorul metodei def change do definim schema care va genera SQL-ul pentru baza de date. create table(:invoices, primary_key: false) do va crea tabelul de facturi . Am setat primary_key: false , dar vom adăuga un câmp ID de tipul UUID, un câmp client de tip text, un câmp de dată de tipul date. Metoda timestamps va genera câmpurile inserted_at și updated_at pe care Ecto le completează automat cu ora în care a fost inserată înregistrarea și, respectiv, cu ora la care a fost actualizată. Acum mergeți la consolă și rulați migrarea:

 mix ecto.migrate

Am creat tabelul invoice cu toate câmpurile definite. Să creăm tabelul cu articole :

 mix ecto.gen.migration create_items

Acum editați scriptul de migrare generat:

 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

Lucrul nou aici este câmpul zecimal care permite numere cu 12 cifre, dintre care 2 sunt pentru partea zecimală a numărului. Să rulăm din nou migrarea:

 mix ecto.migrate

Acum am creat tabelul articole și, în sfârșit, să creăm tabelul invoice_items :

 mix ecto.gen.migration create_invoice_items

Editați migrarea:

 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

După cum puteți vedea, această migrare are câteva părți noi. Primul lucru pe care îl veți observa este add :invoice_id, references(:invoices, type: :uuid, null: false) . Aceasta creează câmpul invoice_id cu o constrângere în baza de date care face referire la tabelul de facturi . Avem același model pentru câmpul item_id . Un alt lucru diferit este modul în care creăm un index: create index(:invoice_items, [:invoice_id]) creează indexul invoice_items_invoice_id_index .

Ecto.Schema și Ecto.Changeset

În Ecto, Ecto.Model a fost depreciat în favoarea utilizării Ecto.Schema , așa că vom numi modulele scheme în loc de modele. Să creăm seturile de modificări. Vom începe cu cel mai simplu element set de modificări și vom crea fișierul 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

În partea de sus, injectăm cod în setul de modificări utilizând use Ecto.Schema . De asemenea, folosim import Ecto.Changeset pentru a importa funcționalitatea din Ecto.Changeset . Am fi putut specifica ce metode specifice să importam, dar haideți să rămânem simplu. alias Cart.InvoiceItem ne permite să scriem direct în setul de modificări InvoiceItem , după cum veți vedea într-un moment.

Ecto.Schema

@primary_key {:id, :binary_id, autogenerate: true} specifică că cheia noastră primară va fi generată automat. Deoarece folosim un tip UUID, definim schema cu schema "items" do și în interiorul blocului definim fiecare câmp și relații. Am definit numele ca șir și prețul ca zecimal, foarte asemănător cu migrarea. Apoi, macro- has_many :invoice_items, InvoiceItem indică o relație între Item și InvoiceItem . Deoarece prin convenție am numit câmpul item_id în tabelul invoice_items , nu trebuie să configuram cheia externă. În cele din urmă, metoda timestamps va seta câmpurile inserted_at și updated_at .

Ecto.Changeset

Funcția def changeset(data, params \\ %{}) do primește o structură Elixir cu parametrii pe care îi vom trece prin diferite funcții. cast(params, @fields) aruncă valorile în tipul corect. De exemplu, puteți trece numai șiruri în parametri și acestea vor fi convertite la tipul corect definit în schemă. validate_required([:name, :price]) validează faptul că câmpurile nume și preț sunt prezente, validate_number(:price, greater_than_or_equal_to: Decimal.new(0)) validează faptul că numărul este mai mare sau egal cu 0 sau, în acest caz, Decimal.new(0) .

În Elixir, operațiunile cu zecimală sunt efectuate diferit, deoarece sunt implementate ca o structură.

A fost mult de luat în considerare, așa că să ne uităm la asta în consolă cu exemple, astfel încât să puteți înțelege mai bine conceptele:

 iex -S mix

Aceasta va încărca consola. -S mix încarcă proiectul curent în 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>

Aceasta returnează o structură Ecto.Changeset care este validă fără erori. Acum să-l salvăm:

 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>}

Nu arătăm SQL-ul pentru concizie. În acest caz, returnează structura Cart.Item cu toate valorile setate. Puteți vedea că inserted_at și updated_at conțin marcajele de timp și câmpul id are o valoare UUID. Sa vedem si alte cazuri:

 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)

Acum am setat elementul Scissors într-un mod diferit, stabilind direct prețul %Cart.Item{price: Decimal.new(20)} . Trebuie să setăm tipul corect, spre deosebire de primul articol în care tocmai am trecut un șir ca preț. Am fi putut trece un float și acesta ar fi fost transformat într-un tip zecimal. Dacă trecem, de exemplu, %Cart.Item{price: 12.5} , atunci când inserați articolul ar arunca o excepție care afirmă că tipul nu se potrivește.

 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>

Pentru a închide consola, apăsați Ctrl+C de două ori. Puteți vedea că validările funcționează și că prețul trebuie să fie mai mare sau egal cu zero (0). După cum puteți vedea, am definit toată schema Ecto.Schema , care este partea legată de modul în care este definită structura modulului și setul de modificări Ecto.Changeset, care este toate validările și turnarea. Să continuăm și să creăm fișierul 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

Acest set de modificări este mai mare, dar ar trebui să fii deja familiarizat cu cele mai multe dintre ele. Aici belongs_to :invoice, Cart.Invoice, type: :binary_id definește relația „aparține” cu setul de modificări Cart.Invoice pe care îl vom crea în curând. Următorul belongs_to :item creează o relație cu tabelul de articole. Am definit @zero Decimal.new(0) . În acest caz, @zero este ca o constantă care poate fi accesată în interiorul modulului. Funcția de set de modificări are părți noi, dintre care una este foreign_key_constraint(:invoice_id, message: "Select a valid invoice") . Acest lucru va permite generarea unui mesaj de eroare în loc să genereze o excepție atunci când constrângerea nu este îndeplinită. Și în sfârșit, metoda set_subtotal va calcula subtotalul. Trecem setul de modificări și returnăm un nou set de modificări cu subtotalul calculat dacă avem atât prețul, cât și cantitatea.

Acum, să creăm Cart.Invoice . Deci, creați și editați fișierul lib/cart/invoice.ex pentru a conține următoarele:

 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

Setul de modificări Cart.Invoice are unele diferențe. Prima este în interiorul schemelor : has_many :invoice_items, InvoiceItem, on_delete: :delete_all înseamnă că atunci când ștergem o factură, toate invoice_items asociate vor fi șterse. Rețineți, totuși, că aceasta nu este o constrângere definită în baza de date.

Să încercăm metoda create în consolă pentru a înțelege mai bine lucrurile. Este posibil să fi creat articolele („Hârtie”, „Foarfece”) pe care le vom folosi aici:

 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) }

Am preluat toate articolele cu Cart.Repo.all și cu funcția Enum.map obținem doar item.id -ul fiecărui articol. În a doua linie, atribuim id1 și id2 cu primul și, respectiv, al doilea item_ids:

 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})

Factura a fost creată cu invoice_items și putem prelua toate facturile acum.

 iex(4)> alias Cart.{Repo, Invoice} iex(5)> Repo.all(Invoice)

Puteți vedea că returnează factura , dar am dori să vedem și invoice_items :

 iex(6)> Repo.all(Invoice) |> Repo.preload(:invoice_items)

Cu funcția Repo.preload , putem obține invoice_items . Rețineți că aceasta poate procesa interogări simultan. În cazul meu, interogarea arăta astfel:

 iex(7)> Repo.get(Invoice, "5d573153-b3d6-46bc-a2c0-6681102dd3ab") |> Repo.preload(:invoice_items)

Ecto.Interogare

Până acum, am arătat cum să creați noi articole și noi facturi cu relații. Dar ce zici de interogare? Ei bine, permiteți-mi să vă prezint Ecto.Query , care ne va ajuta să facem interogări în baza de date, dar mai întâi avem nevoie de mai multe date pentru a explica mai bine.

 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")})

Acum ar trebui să avem 8 articole și există un „ciocolată” repetat. Poate dorim să știm ce elemente se repetă. Deci să încercăm această interogare:

 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"}]

Puteți vedea că în interogare am vrut să returnăm o hartă cu numele articolului și de câte ori apare în tabelul cu articole. Alternativ, totuși, este mai probabil să fim interesați să vedem care sunt cele mai vândute produse. Deci, pentru asta, să creăm niște facturi. Mai întâi, să ne ușurăm viața creând o hartă pentru a accesa un 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"}

După cum puteți vedea, am creat o hartă folosind o înțelegere

 iex(12)> line_items = [%{item_id: items["Chocolates"], quantity: 2}]

Trebuie să adăugăm prețul în parametrii invoice_items pentru a crea o factură, dar ar fi mai bine să trecem doar id-ul articolului și să avem prețul completat automat. Vom face modificări în modulul Cart.Invoice pentru a realiza acest lucru:

 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

Primul lucru pe care îl veți observa este că am adăugat Ecto.Query , care ne va permite să interogăm baza de date. Noua funcție este defp items_with_prices(items) do care caută printre articole și găsește și stabilește prețul pentru fiecare articol.

În primul rând, defp items_with_prices(items) do primește o listă ca argument. Cu item_ids = Enum.map(items, fn(item) -> item[:item_id] || item["item_id"] end) , repetăm ​​toate articolele și obținem doar item_id . După cum puteți vedea, accesăm fie cu atom :item_id , fie cu șirul „item_id”, deoarece hărțile pot avea oricare dintre acestea ca chei. Interogarea q = from(i in Item, select: %{id: i.id, price: i.price}, where: i.id in ^item_ids) va găsi toate articolele care sunt în item_ids și va returna o hartă cu item.id și item.price . Putem rula apoi interogarea prices = Repo.all(q) care returnează o listă de hărți. Apoi trebuie să repetăm ​​articolele și să creăm o nouă listă care va adăuga prețul. Enum.map(items, fn(item) -> iterează prin fiecare articol, găsește prețul Enum.find(prices, fn(p) -> p[:id] == item_id end)[:price] || 0 , și creează o nouă listă cu item_id , cantitate și preț. Și cu asta, nu mai este necesar să adăugați prețul în fiecare dintre invoice_items .

Inserarea mai multor facturi

După cum vă amintiți, mai devreme am creat un element de hartă care ne permite să accesăm id -ul folosind numele articolului pentru, adică items["Gum"] „cb1c5a93-ecbf-4e4b-8588-cc40f7d12364”. Acest lucru simplifică crearea de invoice_items . Să creăm mai multe facturi. Porniți din nou consola și rulați:

 Iex -S mix
 iex(1)> Repo.delete_all(InvoiceItem); Repo.delete_all(Invoice)

Ștergem toate invoice_items și facturile pentru a avea o tablă goală:

 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})

Acum avem 3 facturi; primul cu 2 articole, al doilea cu 3 articole, iar al treilea cu 6 articole. Am dori acum să știm ce produse sunt cele mai vândute articole? Pentru a răspunde la asta, vom crea o interogare pentru a găsi cele mai bine vândute articole după cantitate și subtotal (preț x cantitate).

 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

Importăm Ecto.Query și apoi denumim alias Cart.{InvoiceItem, Item, Repo} așa că nu trebuie să adăugăm Cart la începutul fiecărui modul. Prima funcție items_by_quantity apelează funcția items_by , trecând parametrul :quantity și apelând Repo.all pentru a executa interogarea. Funcția items_by_subtotal este similară cu funcția anterioară, dar trece parametrul :subtotal . Acum să explicăm items_by :

  • from i in Item , această macrocomandă selectează modulul Item
  • join: ii in InvoiceItem, on: ii.item_id == i.id , creează o îmbinare cu condiția „items.id = invoice_items.item_id”
  • select: %{id: i.id, name: i.name, total: sum(field(ii, ^type))} , generăm o hartă cu toate câmpurile pe care le dorim mai întâi selectăm id-ul și numele din Item și facem o sumă de operator. Câmpul (ii, ^tip) utilizează câmpul macro pentru a accesa dinamic un câmp
  • group_by: i.id , Grupăm după articole.id
  • order_by: [desc: sum(field(ii, ^type))] și în final ordonați după sumă în ordine descrescătoare

Până acum am scris interogarea în stilul listă, dar am putea-o rescrie în stil macro:

 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

Prefer să scriu interogări sub formă de listă, deoarece mi se pare mai ușor de citit.

Concluzie

Am acoperit o bună parte din ceea ce puteți face într-o aplicație cu Ecto. Desigur, puteți învăța mult mai multe din documentele Ecto. Cu Ecto, puteți crea aplicații concurente, tolerante la erori, cu puțin efort, care se pot scala cu ușurință datorită mașinii virtuale Erlang. Ecto oferă baza pentru stocarea în aplicațiile dvs. Elixir și oferă funcții și macrocomenzi pentru a vă gestiona cu ușurință datele.

În acest tutorial, am examinat Ecto.Schema, Ecto.Changeset, Ecto.Migration, Ecto.Query și Ecto.Repo. Fiecare dintre aceste module vă ajută în diferite părți ale aplicației dvs. și face codul mai explicit și mai ușor de întreținut și de înțeles.

Dacă doriți să verificați codul tutorialului, îl puteți găsi aici pe GitHub.

Dacă ți-a plăcut acest tutorial și ești interesat de mai multe informații, aș recomanda Phoenix (pentru o listă de proiecte minunate), Awesome Elixir și această discuție care compară ActiveRecord cu Ecto.