Incontra Ecto, wrapper di database senza compromessi per app di elisir simultanee
Pubblicato: 2022-03-11Ecto è un linguaggio specifico del dominio per la scrittura di query e l'interazione con i database nel linguaggio Elisir. L'ultima versione (2.0) supporta PostgreSQL e MySQL. (il supporto per MSSQL, SQLite e MongoDB sarà disponibile in futuro). Nel caso in cui sei nuovo di Elixir o hai poca esperienza con esso, ti consiglio di leggere Guida introduttiva al linguaggio di programmazione Elixir di Kleber Virgilio Correia.
Ecto è composto da quattro componenti principali:
- Ecto.Repo. Definisce i repository che sono wrapper attorno a un datastore. Usandolo, possiamo inserire, creare, eliminare e interrogare un repository. Per comunicare con il database sono necessari un adattatore e le credenziali.
- Ecto.Schema. Gli schemi vengono utilizzati per mappare qualsiasi origine dati in una struttura Elixir.
- Ecto.Changeset. I set di modifiche forniscono agli sviluppatori un modo per filtrare e trasmettere parametri esterni, nonché un meccanismo per tenere traccia e convalidare le modifiche prima che vengano applicate ai dati.
- Ecto.Query. Fornisce una query SQL simile a DSL per il recupero di informazioni da un repository. Le query in Ecto sono sicure, evitano problemi comuni come SQL Injection, pur essendo componibili, consentendo agli sviluppatori di creare query pezzo per pezzo anziché tutte in una volta.
Per questo tutorial, avrai bisogno di:
- Elisir installato (guida all'installazione per 1.2 o versioni successive)
- PostgreSQL installato
- Un utente definito con il permesso di creare un database (Nota: useremo l'utente "postgres" con la password "postgres" come esempio durante questo tutorial.)
Installazione e configurazione
Per cominciare, creiamo una nuova app con un supervisore usando Mix. Mix è uno strumento di compilazione fornito con Elixir che fornisce attività per la creazione, la compilazione, il test dell'applicazione, la gestione delle sue dipendenze e molto altro.
mix new cart --supQuesto creerà un carrello di directory con i file di progetto iniziali:
* 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 Stiamo usando l'opzione --sup poiché abbiamo bisogno di un albero supervisore che manterrà la connessione al database. Successivamente, andiamo nella directory cart con cd cart e apriamo il file mix.exs e sostituiamo il suo contenuto:
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 Nell'applicazione def application do dobbiamo aggiungere come applicazioni :postgrex, :ecto in modo che possano essere utilizzate all'interno della nostra applicazione. Dobbiamo anche aggiungerli come dipendenze aggiungendo in defp deps do postgrex (che è l'adattatore del database) ed ecto . Dopo aver modificato il file, eseguire nella console:
mix deps.get Questo installerà tutte le dipendenze e creerà un file mix.lock che memorizza tutte le dipendenze e le sotto-dipendenze dei pacchetti installati (simile a Gemfile.lock nel bundler).
Ecto.Repo
Vedremo ora come definire un repository nella nostra applicazione. Possiamo avere più di un repository, il che significa che possiamo connetterci a più di un database. Dobbiamo configurare il database nel file config/config.exs :
use Mix.Config config :cart, ecto_repos: [Cart.Repo] Stiamo solo impostando il minimo, quindi possiamo eseguire il comando successivo. Con la riga :cart, cart_repos: [Cart.Repo] stiamo dicendo a Ecto quali repository stiamo usando. Questa è una caratteristica interessante poiché ci consente di avere molti repository, ovvero possiamo connetterci a più database.
Ora esegui il seguente comando:
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] Questo comando genera il repository. Se leggi l'output, ti dice di aggiungere un supervisore e un repository nella tua app. Cominciamo dal supervisore. 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 In questo file, definiamo il supervisore supervisor(Cart.Repo, []) e lo aggiungiamo all'elenco dei figli (in Elixir, gli elenchi sono simili agli array). Definiamo i bambini supervisionati con la strategia della strategy: :one_for_one che significa che, se uno dei processi supervisionati fallisce, il supervisore riavvierà solo quel processo nel suo stato predefinito. Puoi saperne di più sui supervisori qui. Se guardi lib/cart/repo.ex vedrai che questo file è già stato creato, il che significa che abbiamo un Repo per la nostra applicazione.
defmodule Cart.Repo do use Ecto.Repo, otp_app: :cart end Ora modifichiamo il file di configurazione 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"Dopo aver definito tutta la configurazione per il nostro database, ora possiamo generarlo eseguendo:
mix ecto.createQuesto comando crea il database e, con questo, abbiamo sostanzialmente terminato la configurazione. Ora siamo pronti per iniziare la codifica, ma definiamo prima l'ambito della nostra app.
Creazione di una fattura con articoli in linea
Per la nostra applicazione demo, creeremo un semplice strumento di fatturazione. Per i changeset (modelli) avremo Invoice , Item e InvoiceItem . InvoiceItem appartiene a Invoice e Item . Questo diagramma rappresenta come i nostri modelli saranno correlati tra loro:
Il diagramma è piuttosto semplice. Abbiamo una tabella fatture che ha molti articoli_fattura in cui memorizziamo tutti i dettagli e anche una tabella articoli che ha molti articoli_fattura . Puoi vedere che il tipo per id_fattura e id_articolo nella tabella articoli_fattura è UUID. Utilizziamo UUID perché aiuta a offuscare i percorsi, nel caso in cui desideri esporre l'app su un'API e semplifica la sincronizzazione poiché non dipendi da un numero sequenziale. Ora creiamo le tabelle usando le attività Mix.
Ecto.Migrazione
Le migrazioni sono file utilizzati per modificare lo schema del database. Ecto.Migration ti offre una serie di metodi per creare tabelle, aggiungere indici, creare vincoli e altre cose relative allo schema. Le migrazioni aiutano davvero a mantenere l'applicazione sincronizzata con il database. Creiamo uno script di migrazione per la nostra prima tabella:
mix ecto.gen.migration create_invoices Questo genererà un file simile a priv/repo/migrations/20160614115844_create_invoices.exs dove definiremo la nostra migrazione. Aprire il file generato e modificarne il contenuto in modo che sia il seguente:
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 All'interno del metodo def change do definiamo lo schema che genererà l'SQL per il database. create table(:invoices, primary_key: false) do creerà la tabella fatture . Abbiamo impostato primary_key: false ma aggiungeremo un campo ID di tipo UUID, campo cliente di tipo testo, campo data di tipo data. Il metodo timestamps genererà i campi inserted_at e updated_at che Ecto riempie automaticamente rispettivamente con l'ora in cui è stato inserito il record e l'ora in cui è stato aggiornato. Ora vai alla console ed esegui la migrazione:
mix ecto.migrate Abbiamo creato la tabella invoice con tutti i campi definiti. Creiamo la tabella degli articoli :
mix ecto.gen.migration create_itemsOra modifica lo script di migrazione generato:
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 endLa novità qui è il campo decimale che consente numeri con 12 cifre, 2 delle quali sono per la parte decimale del numero. Eseguiamo nuovamente la migrazione:
mix ecto.migrateOra abbiamo creato la tabella degli articoli e infine creiamo la tabella fattura_articoli :
mix ecto.gen.migration create_invoice_itemsModifica la migrazione:
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 Come puoi vedere, questa migrazione ha alcune nuove parti. La prima cosa che noterai è add :invoice_id, references(:invoices, type: :uuid, null: false) . Questo crea il campo fattura_id con un vincolo nel database che fa riferimento alla tabella fatture . Abbiamo lo stesso schema per il campo item_id . Un'altra cosa diversa è il modo in cui creiamo un indice: create index(:invoice_items, [:invoice_id]) crea l'indice fattura_items_invoice_id_index .
Ecto.Schema e Ecto.Changeset
In Ecto, Ecto.Model è stato deprecato a favore dell'utilizzo di Ecto.Schema , quindi chiameremo i moduli schemi anziché modelli. Creiamo i changeset. Inizieremo con il changeset Item più semplice e creeremo il file 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 Nella parte superiore, inseriamo il codice nel changeset usando use Ecto.Schema . Stiamo anche usando import Ecto.Changeset per importare funzionalità da Ecto.Changeset . Avremmo potuto specificare quali metodi specifici importare, ma manteniamo le cose semplici. L' alias Cart.InvoiceItem ci permette di scrivere direttamente all'interno del changeset InvoiceItem , come vedrai tra poco.
Ecto.Schema
La @primary_key {:id, :binary_id, autogenerate: true} specifica che la nostra chiave primaria verrà generata automaticamente. Poiché stiamo usando un tipo UUID, definiamo lo schema con lo schema "items" do e all'interno del blocco definiamo ogni campo e le relazioni. Abbiamo definito il nome come stringa e il prezzo come decimale, molto simile alla migrazione. Successivamente, la macro has_many :invoice_items, InvoiceItem indica una relazione tra Item e InvoiceItem . Poiché per convenzione abbiamo chiamato il campo item_id nella tabella fattura_items , non è necessario configurare la chiave esterna. Infine il metodo timestamp imposterà i campi insert_at e update_at .
Ecto.Changeset
La funzione def changeset(data, params \\ %{}) do riceve una struttura Elixir con parametri che passeremo attraverso diverse funzioni. cast(params, @fields) il cast dei valori nel tipo corretto. Ad esempio, puoi passare solo stringhe nei parametri e quelle verrebbero convertite nel tipo corretto definito nello schema. validate_required([:name, :price]) verifica che i campi nome e prezzo siano presenti, validate_number(:price, greater_than_or_equal_to: Decimal.new(0)) che il numero sia maggiore o uguale a 0 o in questo caso Decimal.new(0) .
È stato molto da capire, quindi diamo un'occhiata alla console con esempi in modo da poter cogliere meglio i concetti:
iex -S mix Questo caricherà la console. -S mix carica il progetto corrente in 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> Ciò restituisce una struttura Ecto.Changeset valida senza errori. Ora salviamolo:
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>}Non mostriamo l'SQL per brevità. In questo caso, restituisce la struttura Cart.Item con tutti i valori impostati, puoi vedere che insert_at e aggiornato_at contengono i loro timestamp e il campo id ha un valore UUID. Vediamo altri casi:
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) Ora abbiamo impostato l'elemento Scissors in un modo diverso, impostando il prezzo direttamente %Cart.Item{price: Decimal.new(20)} . Dobbiamo impostare il tipo corretto, a differenza del primo elemento in cui abbiamo appena passato una stringa come prezzo. Avremmo potuto passare un float e questo sarebbe stato convertito in un tipo decimale. Se passiamo, ad esempio %Cart.Item{price: 12.5} , quando si inserisce l'elemento viene generata un'eccezione che indica che il tipo non corrisponde.

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> Per terminare la console, premere Ctrl+C due volte. Puoi vedere che le convalide funzionano e il prezzo deve essere maggiore o uguale a zero (0). Come puoi vedere, abbiamo definito tutto lo schema Ecto.Schema che è la parte relativa a come viene definita la struttura del modulo e il changeset Ecto.Changeset che è tutto validazioni e casting. Continuiamo e creiamo il file 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 Questo set di modifiche è più grande ma dovresti già avere familiarità con la maggior parte di esso. Qui belongs_to :invoice, Cart.Invoice, type: :binary_id definisce la relazione "appartiene a" con il changeset Cart.Invoice che creeremo presto. Il successivo belongs_to :item crea una relazione con la tabella degli elementi. Abbiamo definito @zero Decimal.new(0) . In questo caso, @zero è come una costante a cui è possibile accedere all'interno del modulo. La funzione changeset ha nuove parti, una delle quali è foreign_key_constraint(:invoice_id, message: "Select a valid invoice") . Ciò consentirà di generare un messaggio di errore invece di generare un'eccezione quando il vincolo non è soddisfatto. Infine, il metodo set_subtotal calcolerà il totale parziale. Passiamo il changeset e restituiamo un nuovo changeset con il subtotale calcolato se abbiamo sia il prezzo che la quantità.
Ora creiamo la Cart.Invoice . Quindi crea e modifica il file lib/cart/invoice.ex per contenere quanto segue:
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 Il set di modifiche Cart.Invoice presenta alcune differenze. Il primo è all'interno degli schemi : has_many :invoice_items, InvoiceItem, on_delete: :delete_all significa che quando eliminiamo una fattura, tutti gli elementi_fattura associati verranno eliminati. Tieni presente, tuttavia, che questo non è un vincolo definito nel database.
Proviamo il metodo create nella console per capire meglio le cose. Potresti aver creato gli elementi ("Carta", "Forbici") che useremo qui:
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) } Abbiamo recuperato tutti gli articoli con Cart.Repo.all e con la funzione Enum.map otteniamo semplicemente l' item.id di ogni articolo. Nella seconda riga, assegniamo semplicemente id1 e id2 con il primo e il secondo item_ids, rispettivamente:
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})La fattura è stata creata con le relative fatture_items e ora possiamo recuperare tutte le fatture.
iex(4)> alias Cart.{Repo, Invoice} iex(5)> Repo.all(Invoice)Puoi vedere che restituisce la fattura ma vorremmo vedere anche fattura_articoli :
iex(6)> Repo.all(Invoice) |> Repo.preload(:invoice_items) Con la funzione Repo.preload , possiamo ottenere invoice_items . Tieni presente che questo può elaborare le query contemporaneamente. Nel mio caso la query era simile a questa:
iex(7)> Repo.get(Invoice, "5d573153-b3d6-46bc-a2c0-6681102dd3ab") |> Repo.preload(:invoice_items)Ecto.Query
Finora abbiamo mostrato come creare nuovi articoli e nuove fatture con relazioni. Ma per quanto riguarda l'interrogazione? Bene, lascia che ti presenti Ecto.Query che ci aiuterà a fare query al database, ma prima abbiamo bisogno di più dati per spiegare meglio.
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")})Ora dovremmo avere 8 articoli e c'è un "Cioccolato" ripetuto. Potremmo voler sapere quali elementi vengono ripetuti. Quindi proviamo questa query:
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"}] Puoi vedere che nella query volevamo restituire una mappa con il nome dell'articolo e il numero di volte in cui appare nella tabella degli articoli. In alternativa, però, potremmo essere più probabilmente interessati a vedere quali sono i prodotti più venduti. Quindi, per questo, creiamo alcune fatture. Innanzitutto, semplifichiamoci la vita creando una mappa per accedere a 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"}Come puoi vedere abbiamo creato una mappa utilizzando una comprensione
iex(12)> line_items = [%{item_id: items["Chocolates"], quantity: 2}] Dobbiamo aggiungere il prezzo nei parametri invoice_items per creare una fattura, ma sarebbe meglio semplicemente passare l'id dell'articolo e fare riempire automaticamente il prezzo. Apporteremo modifiche al modulo Cart.Invoice per ottenere ciò:
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 La prima cosa che noterai è che abbiamo aggiunto Ecto.Query , che ci permetterà di interrogare il database. La nuova funzione è defp items_with_prices(items) do che ricerca tra gli articoli e trova e imposta il prezzo per ogni articolo.
Innanzitutto, defp items_with_prices(items) do riceve un elenco come argomento. Con item_ids = Enum.map(items, fn(item) -> item[:item_id] || item["item_id"] end) , ripetiamo tutti gli elementi e otteniamo solo item_id . Come puoi vedere, accediamo con atom :item_id o con la stringa "item_id", poiché le mappe possono avere una di queste come chiavi. La query q = from(i in Item, select: %{id: i.id, price: i.price}, where: i.id in ^item_ids) troverà tutti gli elementi che si trovano in item_ids e restituirà una mappa con item.id e item.price . Possiamo quindi eseguire la query prices = Repo.all(q) che restituisce un elenco di mappe. Dobbiamo quindi scorrere gli articoli e creare un nuovo elenco che aggiungerà il prezzo. Enum.map(items, fn(item) -> scorre ogni articolo, trova il prezzo Enum.find(prices, fn(p) -> p[:id] == item_id end)[:price] || 0 , e crea un nuovo elenco con item_id , quantità e prezzo. Con ciò, non è più necessario aggiungere il prezzo in ciascuno dei invoice_items .
Inserimento di più fatture
Come ricorderete, in precedenza abbiamo creato una mappa degli elementi che ci consente di accedere all'ID utilizzando il nome dell'elemento per es items["Gum"] “cb1c5a93-ecbf-4e4b-8588-cc40f7d12364”. Ciò semplifica la creazione di fatture_articoli . Creiamo più fatture. Riavvia la console ed esegui:
Iex -S mix iex(1)> Repo.delete_all(InvoiceItem); Repo.delete_all(Invoice)Eliminiamo tutti gli elementi_fattura e le fatture per avere una tabula rasa:
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})Ora abbiamo 3 fatture; il primo con 2 elementi, il secondo con 3 elementi e il terzo con 6 elementi. Ora vorremmo sapere quali prodotti sono gli articoli più venduti? Per rispondere, creeremo una query per trovare gli articoli più venduti in base alla quantità e al totale parziale (prezzo x quantità).
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 Importiamo Ecto.Query e quindi alias Cart.{InvoiceItem, Item, Repo} quindi non è necessario aggiungere Carrello all'inizio di ogni modulo. La prima funzione items_by_quantity chiama la funzione items_by , passando il parametro :quantity e chiamando Repo.all per eseguire la query. La funzione items_by_subtotal è simile alla funzione precedente ma passa il parametro :subtotal . Ora spieghiamo items_by :
-
from i in Item, questa macro seleziona il modulo Item -
join: ii in InvoiceItem, on: ii.item_id == i.id, crea un join alla condizione "items.id = fattura_items.item_id" -
select: %{id: i.id, name: i.name, total: sum(field(ii, ^type))}, stiamo generando una mappa con tutti i campi che vogliamo prima selezioniamo l'id e il nome da Item e facciamo una somma di operatori. Il campo(ii, ^tipo) utilizza il campo della macro per accedere dinamicamente a un campo -
group_by: i.id, raggruppiamo per items.id -
order_by: [desc: sum(field(ii, ^type))]e infine ordina per la somma in ordine decrescente
Finora abbiamo scritto la query in stile elenco ma potremmo riscriverla in stile 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))]) endPreferisco scrivere le query in forma di elenco poiché lo trovo più leggibile.
Conclusione
Abbiamo coperto buona parte di ciò che puoi fare in un'app con Ecto. Naturalmente, c'è molto di più che puoi imparare dai documenti di Ecto. Con Ecto, puoi creare applicazioni simultanee tolleranti ai guasti con poco sforzo che possono essere facilmente scalabili grazie alla macchina virtuale Erlang. Ecto fornisce la base per l'archiviazione nelle tue applicazioni Elixir e fornisce funzioni e macro per gestire facilmente i tuoi dati.
In questo tutorial, abbiamo esaminato Ecto.Schema, Ecto.Changeset, Ecto.Migration, Ecto.Query ed Ecto.Repo. Ognuno di questi moduli ti aiuta in diverse parti della tua applicazione e rende il codice più esplicito e più facile da gestire e comprendere.
Se vuoi dare un'occhiata al codice del tutorial, puoi trovarlo qui su GitHub.
Se ti è piaciuto questo tutorial e sei interessato a maggiori informazioni, consiglierei Phoenix (per un elenco di fantastici progetti), Awesome Elixir e questo discorso che confronta ActiveRecord con Ecto.
