Ecto je jezik domene za pisanje poizvedb in interakcijo z bazami podatkov v Eliksir jezik . Najnovejša različica (2.0) podpira PostgreSQL in MySQL. (podpora za MSSQL, SQLite in MongoDB bo na voljo v prihodnosti). V primeru, da ste novi Eliksir ali imate malo izkušenj s tem, priporočam, da preberete Kleber Virgilio Correia Uvod v programski jezik Elixir .
Ecto je sestavljen iz štirih glavnih komponent:
Za to vadnico boste potrebovali:
Za začetek ustvarimo novo aplikacijo z nadzornikom s pomočjo Mix. Zmešajte je orodje za izdelavo, ki je priloženo Elixirju in ponuja naloge za ustvarjanje, sestavljanje, preizkušanje aplikacije, upravljanje odvisnosti in še veliko več.
mix new cart --sup
Tako boste ustvarili imeniški voziček z začetnimi datotekami projekta:
* 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
Uporabljamo --sup
možnost, ker potrebujemo nadzorniško drevo, ki bo ohranilo povezavo z bazo podatkov. Nato gremo na cart
imenik z cd cart
in odprite datoteko mix.exs
in nadomestite njegovo vsebino:
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
V def application do
kot aplikacije moramo dodati :postgrex, :ecto
tako da jih lahko uporabimo v naši aplikaciji. Te moramo dodati tudi kot odvisnosti, tako da dodamo defp deps do
postgrex (ki je vmesnik baze podatkov) in ekto . Ko uredite datoteko, zaženite v konzoli:
mix deps.get
S tem boste namestili vse odvisnosti in ustvarili datoteko mix.lock
ki shrani vse odvisnosti in pododvisnosti nameščenih paketov (podobno Gemfile.lock
v paketu).
Zdaj bomo preučili, kako določiti repo v naši aplikaciji. Lahko imamo več repo, kar pomeni, da se lahko povežemo z več bazami podatkov. Bazo podatkov moramo konfigurirati v datoteki config/config.exs
:
use Mix.Config config :cart, ecto_repos: [Cart.Repo]
Pravkar postavljamo minimum, zato lahko zaženemo naslednji ukaz. S črto :cart, cart_repos: [Cart.Repo]
Ecto-ju povemo, katere repo posnetke uporabljamo. To je čudovita lastnost, saj nam omogoča, da imamo veliko repozitov, torej se lahko povežemo z več bazami podatkov.
Zdaj zaženite naslednji ukaz:
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]
Ta ukaz ustvari repo. Če preberete izhod, vam sporoči, da v svojo aplikacijo dodate nadzornika in repo. Začnimo z nadzornikom. Uredili bomo 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
V tej datoteki definiramo nadzornika supervisor(Cart.Repo, [])
in dodajanje na seznam otrok (v Elixirju so seznami podobni nizom). Določimo otroke, ki jih nadziramo s strategijo strategy: :one_for_one
kar pomeni, da bo nadzornik v primeru, da kateri od nadzorovanih procesov ne bo uspel, znova zagnal le ta v njegovo privzeto stanje. Več o nadzornikih lahko izveste več tukaj . Če pogledate lib/cart/repo.ex
videli boste, da je bila ta datoteka že ustvarjena, kar pomeni, da imamo Repo za našo prijavo.
defmodule Cart.Repo do use Ecto.Repo, otp_app: :cart end
Zdaj pa uredimo konfiguracijsko datoteko 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'
Po opredelitvi celotne konfiguracije za našo bazo podatkov jo lahko zdaj ustvarimo tako, da zaženemo:
mix ecto.create
Ta ukaz ustvari bazo podatkov in s tem smo v bistvu končali konfiguracijo. Zdaj smo pripravljeni začeti s kodiranjem, vendar najprej določimo obseg naše aplikacije.
Za našo predstavitveno aplikacijo bomo izdelali preprosto orodje za izdajanje računov. Za nabore sprememb (modele) bomo imeli Račun , Postavka in Predmet računa . Predmet računa pripada Račun in Postavka . Ta diagram predstavlja, kako bodo naši modeli med seboj povezani:
Diagram je precej preprost. Imamo mizo računi ki jih ima veliko item_items kamor shranimo vse podrobnosti in tudi mizo predmetov ki jih ima veliko item_items . Vidite, da je tip za račun_id in item_id v item_items tabela je UUID. UUID uporabljamo, ker pomaga zakrivati poti, če želite aplikacijo izpostaviti prek API-ja in poenostaviti sinhronizacijo, saj niste odvisni od zaporedne številke. Zdaj pa ustvarimo tabele z uporabo nalog Mix.
Selitve so datoteke, ki se uporabljajo za spreminjanje sheme baze podatkov. Ecto.Migration vam daje nabor metod za ustvarjanje tabel, dodajanje indeksov, ustvarjanje omejitev in drugih stvari, povezanih s shemo. Selitve resnično pomagajo sinhronizirati aplikacijo z bazo podatkov. Ustvarimo skript za selitev za našo prvo tabelo:
mix ecto.gen.migration create_invoices
S tem bo ustvarjena datoteka, podobna priv/repo/migrations/20160614115844_create_invoices.exs
kjer bomo opredelili svojo migracijo. Odprite ustvarjeno datoteko in spremenite njeno vsebino, kot sledi:
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
Notranja metoda def change do
definiramo shemo, ki bo generirala SQL za bazo podatkov. create table(:invoices, primary_key: false) do
bo ustvaril tabelo računi . Nastavili smo primary_key: false
bomo pa dodali polje tipa ID UUID , polje kupca besedila vrste, datum polje polja datuma. The timestamps
metoda bo ustvarila polja inserted_at
in updated_at
da se Ecto samodejno napolni s časom vstavitve zapisa in časom posodobitve. Zdaj pojdite na konzolo in zaženite selitev:
mix ecto.migrate
Ustvarili smo tabelo invoice
s z vsemi definiranimi polji. Ustvarimo predmetov tabela:
mix ecto.gen.migration create_items
Zdaj uredite ustvarjeni skript za selitev:
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
Novost pri tem je decimalno polje, ki omogoča števila z 12 števkami, od tega 2 za decimalni del števila. Ponovno zaženimo selitev:
mix ecto.migrate
Zdaj smo ustvarili predmetov tabelo in končno ustvarimo item_items tabela:
mix ecto.gen.migration create_invoice_items
Uredi selitev:
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
Kot lahko vidite, ima ta selitev nekaj novih delov. Prva stvar, ki jo boste opazili, je add :invoice_id, references(:invoices, type: :uuid, null: false)
. To ustvari polje račun_id z omejitvijo v zbirki podatkov, ki se sklicuje na računi tabela. Imamo enak vzorec za item_id polje. Druga stvar, ki je drugačna, je način, kako ustvarimo indeks: create index(:invoice_items, [:invoice_id])
ustvari indeks invoice_items_invoice_id_index .
V Ecto, Ecto.Model
je opuščen v korist uporabe Ecto.Schema
, zato bomo namesto modelov poklicali sheme modulov. Ustvarimo nabore sprememb. Začeli bomo z najpreprostejšim elementom nabora sprememb in ustvarili datoteko 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
Na vrh vbrizgamo kodo v nabor sprememb z use Ecto.Schema
. Uporabljamo tudi import Ecto.Changeset
za uvoz funkcionalnosti iz Ecto.Changeset . Lahko bi določili, katere posebne metode želite uvoziti, vendar naj bo preprosto. The alias Cart.InvoiceItem
nam omogoča pisanje neposredno znotraj nabora sprememb Predmet računa , kot boste videli čez trenutek.
The @primary_key {:id, :binary_id, autogenerate: true}
določa, da bo naš primarni ključ samodejno ustvarjen. Ker uporabljamo tip UUID, shemo definiramo z schema 'items' do
znotraj bloka pa definiramo vsako polje in razmerja. Določili smo ime kot niz in cena kot decimalna številka, zelo podobna selitvi. Nato makro has_many :invoice_items, InvoiceItem
označuje razmerje med Postavka in Predmet računa . Ker smo po dogovoru poimenovali področje item_id v item_items tabele, nam ni treba konfigurirati tujega ključa. Končno časovne žige metoda bo nastavila vstavljeno_at in updated_at polja.
The def changeset(data, params \ %{}) do
funkcija prejme Elixirjevo strukturo s parametri, ki jih bomo cev prek različnih funkcij. cast(params, @fields)
odda vrednosti v pravi tip. Na primer, lahko v parametre prenesete samo nize, ki bi bili pretvorjeni v pravi tip, definiran v shemi. validate_required([:name, :price])
potrjuje, da ime in cena polja so prisotna, validate_number(:price, greater_than_or_equal_to: Decimal.new(0))
potrdi, da je število večje ali enako 0 ali v tem primeru Decimal.new(0)
.
To je bilo veliko za prevzeti, zato si oglejmo to v konzoli s primeri, da boste lahko bolje razumeli koncepte:
iex -S mix
To bo naložilo konzolo. -S mix
naloži trenutni projekt v iex REPL.
iex(0)> item = Cart.Item.changeset(%Cart.Item{}, %{name: 'Paper', price: '2.5'}) #Ecto.Changeset
To vrne Ecto.Changeset
strukturo, ki je veljavna brez napak. Zdaj pa ga shranimo:
iex(1)> item = Cart.Repo.insert!(item) %Cart.Item{__meta__: #Ecto.Schema.Metadata, id: '66ab2ab7-966d-4b11-b359-019a422328d7', inserted_at: #Ecto.DateTime, invoice_items: #Ecto.Association.NotLoaded, name: 'Paper', price: #Decimal, updated_at: #Ecto.DateTime}
SQL ne prikazujemo kratkosti. V tem primeru vrne Košarica struct z vsemi nastavljenimi vrednostmi, to lahko vidite vstavljeno_at in updated_at vsebujejo njihove časovne žige in id polje ima vrednost UUID. Poglejmo še nekaj primerov:
iex(3)> item2 = Cart.Item.changeset(%Cart.Item{price: Decimal.new(20)}, %{name: 'Scissors'}) #Ecto.Changeset iex(4)> Cart.Repo.insert(item2)
Zdaj smo nastavili Scissors
artikla na drugačen način, pri čemer določite ceno neposredno %Cart.Item{price: Decimal.new(20)}
. Za razliko od prvega elementa, kjer smo pravkar poslali niz kot ceno, moramo nastaviti njegovo pravilno vrsto. Lahko bi prenesli float in to bi bilo oddano v decimalno vrsto. Če prenesemo, na primer %Cart.Item{price: 12.5}
, ko vstavite element, bo vrgla izjema, ki navaja, da se vrsta ne ujema.
iex(4)> invalid_item = Cart.Item.changeset(%Cart.Item{}, %{name: 'Scissors', price: -1.5}) #Ecto.Changeset
Če želite končati konzolo, dvakrat pritisnite Ctrl + C. Vidite lahko, da preverjanja veljajo in mora biti cena večja ali enaka nič (0). Kot lahko vidite, smo definirali vse sheme Ecto.Schema kar je del, povezan s tem, kako je definirana struktura modula in nabor sprememb Ecto.Changeset kar je vse validacije in vlivanje. Nadaljujmo in ustvarimo datoteko 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.data.price), (cs.changes[:quantity] do {_price, nil} -> cs {nil, _quantity} -> cs {price, quantity} -> put_change(cs, :subtotal, Decimal.mult(price, quantity)) end end end
Ta nabor sprememb je večji, vendar bi ga morali že poznati. Tukaj belongs_to :invoice, Cart.Invoice, type: :binary_id
opredeljuje odnos 'pripada' z Košarica.Račun nabor sprememb, ki ga bomo kmalu ustvarili. Naslednji belongs_to :item
ustvari odnos s tabelo elementov. Določili smo @zero Decimal.new(0)
. V tem primeru, @zero je kot konstanta, do katere je mogoče dostopati znotraj modula. Funkcija nabora sprememb ima nove dele, med katerimi je foreign_key_constraint(:invoice_id, message: 'Select a valid invoice')
. To bo omogočilo ustvarjanje sporočila o napaki, namesto da bi ustvarili izjemo, kadar omejitev ni izpolnjena. In končno metoda set_subtotal bo izračunal vmesni seštevek. Prepustimo nabor sprememb in vrnemo nov nabor sprememb z vmesnim seštevkom, izračunanim, če imamo tako ceno kot količino.
Zdaj pa ustvarimo Košarica.Račun . Torej ustvarite in uredite datoteko lib/cart/invoice.ex
vsebovati naslednje:
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
Košarica.Račun changeset ima nekaj razlik. Prva je notri sheme : has_many :invoice_items, InvoiceItem, on_delete: :delete_all
pomeni, da ko izbrišemo račun, vsi povezani item_items bo izbrisan. Vendar ne pozabite, da to ni omejitev, ki je definirana v zbirki podatkov.
Preizkusimo metodo create v konzoli, da stvari bolje razumemo. Morda ste ustvarili elemente ('Paper', 'Škarje'), ki jih bomo uporabili tukaj:
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) }
Vse predmete smo prenesli z Cart.Repo.all in z Enum.map funkcijo pravkar dobimo item.id
vsakega predmeta. V drugi vrstici samo dodelimo id1
in id2
s prvim in drugim item_id:
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})
Račun je bil ustvarjen s svojimi računi_postavke in vse račune lahko dobimo zdaj.
iex(4)> alias Cart.{Repo, Invoice} iex(5)> Repo.all(Invoice)
Vidite, da vrne Račun vendar bi radi videli tudi item_items :
iex(6)> Repo.all(Invoice) |> Repo.preload(:invoice_items)
Z Repo.preload funkcijo, lahko dobimo invoice_items
. Upoštevajte, da lahko s tem poizvedbe obdelujete hkrati. V mojem primeru je bila poizvedba videti tako:
iex(7)> Repo.get(Invoice, '5d573153-b3d6-46bc-a2c0-6681102dd3ab') |> Repo.preload(:invoice_items)
Do zdaj smo pokazali, kako z razmerji ustvariti nove postavke in nove račune. Kaj pa poizvedovanje? No, naj vam predstavim Ecto.Query kar nam bo pomagalo poizvedovati v zbirko podatkov, vendar najprej potrebujemo več podatkov, da jih bolje pojasnimo.
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')})
Zdaj bi morali imeti 8 predmetov in ponavlja se 'Čokolada'. Morda bomo želeli vedeti, kateri elementi se ponavljajo. Preizkusimo torej to poizvedbo:
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'}]
Vidite lahko, da smo v poizvedbi želeli vrniti zemljevid z imenom predmeta in številom pojavitev v tabeli elementov. Vendar pa bi nas verjetno verjetno zanimalo, kateri so najbolj prodajani izdelki. Torej, za to ustvarimo nekaj računov. Najprej si olajmo življenje z ustvarjanjem zemljevida za dostop do item_id
:
iex(10)> l = Repo.all(from(i in Item, select: {i.name, i.id})) iex(11)> items = for {k, v} '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'}
Kot lahko vidite, smo ustvarili zemljevid z uporabo razumevanje
iex(12)> line_items = [%{item_id: items['Chocolates'], quantity: 2}]
Ceno moramo dodati v invoice_items
params, da ustvarite račun, vendar bi bilo bolje, če samo posredujete ID izdelka in samodejno izpolnite ceno. Spremenili bomo Košarica.Račun modul za to:
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'] % end) end
Prvo, kar boste opazili, je, da smo dodali Ecto.Query , ki nam bo omogočil poizvedbo po zbirki podatkov. Nova funkcija je defp items_with_prices(items) do
ki išče po artiklih ter za vsak artikel najde in določi ceno.
Najprej, defp items_with_prices(items) do
prejme seznam kot argument. Z item_ids = Enum.map(items, fn(item) -> item[:item_id] || item['item_id'] end)
prelistamo vse elemente in dobimo samo item_id . Kot lahko vidite, do obeh dostopamo z atomom :item_id
ali niz “item_id”, saj imajo lahko zemljevidi katero koli od teh tipk. Poizvedba q = from(i in Item, select: %{id: i.id, price: i.price}, where: i.id in ^item_ids)
bo našel vse elemente v item_ids
in bo vrnil zemljevid z item.id
in item.price
. Nato lahko izvedemo poizvedbo prices = Repo.all(q)
ki vrne seznam zemljevidov. Nato moramo pregledati elemente in ustvariti nov seznam, ki bo dodal ceno. The Enum.map(items, fn(item) ->
pregleduje vsak artikel, poišče ceno Enum.find(prices, fn(p) -> p[:id] == item_id end)[:price] || 0
in ustvari nov seznam z item_id
, količino in ceno. In s tem ni več treba dodajati cene v vsakem od invoice_items
.
Kot se spomnite, prej smo ustvarili zemljevid predmetov ki nam omogoča dostop do id z uporabo imena elementa za i.e items['Gum']
“Cb1c5a93-ecbf-4e4b-8588-cc40f7d12364”. To olajša ustvarjanje item_items . Ustvarimo več računov. Znova zaženite konzolo in zaženite:
Iex -S mix
iex(1)> Repo.delete_all(InvoiceItem); Repo.delete_all(Invoice)
Izbrišemo vse item_items in računi za prazen list:
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})
Zdaj imamo 3 račune; prvi z 2 postavkama, drugi s 3 predmeti in tretji s 6 predmeti. Zdaj bi radi vedeli, kateri izdelki so najbolje prodajani izdelki? Da bi odgovorili na to, bomo ustvarili poizvedbo za iskanje najbolje prodajanih izdelkov po količini in vmesnih seštevkih (cena x količina).
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
Uvažamo Ecto.Query in potem smo alias Cart.{InvoiceItem, Item, Repo}
zato nam ni treba dodati košarice na začetku vsakega modula. Prva funkcija items_by_quantity pokliče items_by
funkcija, ki posreduje :quantity
in klic parametra Repo.all za izvedbo poizvedbe. Funkcija items_by_subtotal je podobna prejšnji funkciji, vendar posreduje :subtotal
parameter. Zdaj pa razložimo items_by :
from i in Item
, ta makro izbere modul Elementjoin: ii in InvoiceItem, on: ii.item_id == i.id
, ustvari združevanje pod pogojem 'items.id = invoice_items.item_id'select: %{id: i.id, name: i.name, total: sum(field(ii, ^type))}
, ustvarjamo zemljevid z vsemi polji, ki jih želimo, najprej izberemo id in ime iz Elementa in naredimo vsoto operaterja. Polje (tip ii, ^) uporablja polje makra za dinamični dostop do poljagroup_by: i.id
, Razvrščamo po items.idorder_by: [desc: sum(field(ii, ^type))]
in nazadnje razvrsti po vsoti v padajočem vrstnem reduDo zdaj smo poizvedbo napisali v slogu seznama, vendar bi ga lahko prepisali v slogu makra:
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
Poizvedbe raje pišem v obliki seznama, saj se mi zdi bolj berljiva.
Dober del tega, kar lahko storite v aplikaciji z Ecto, smo zajeli. Seveda se lahko še veliko več naučite iz Ecto docs . Z Ecto lahko ustvarite sočasne aplikacije, odporne na napake, z malo truda, ki jih je mogoče enostavno prilagoditi zahvaljujoč virtualnemu stroju Erlang. Ecto zagotavlja osnovo za shranjevanje v vaših aplikacijah Elixir ter funkcije in makre za enostavno upravljanje podatkov.
V tej vadnici smo preučili Ecto.Schema , Ecto.Changeset , Ecto.Migration , Ecto.Query , in Ecto.Repo . Vsak od teh modulov vam pomaga v različnih delih vaše aplikacije in naredi kodo bolj eksplicitno in enostavnejšo za vzdrževanje in razumevanje.
Če želite preveriti kodo vadnice, jo lahko najdete tukaj na GitHub.
Če vam je bila ta vadnica všeč in vas zanima več informacij, vam priporočam Phoenix (za seznam odličnih projektov), Izjemen eliksir , in ta pogovor ki primerja ActiveRecord z Ecto.