Екто е специфичен за домейн език за писане на заявки и взаимодействие с бази данни в Еликсирен език . Най-новата версия (2.0) поддържа PostgreSQL и MySQL. (поддръжката за MSSQL, SQLite и MongoDB ще бъде налична в бъдеще). В случай, че сте нов за Еликсир или имате малко опит с него, бих ви препоръчал да прочетете Kleber Virgilio Correia’s Първи стъпки с езика за програмиране Elixir .
Ecto се състои от четири основни компонента:
За този урок ще ви трябва:
Като начало, нека създадем ново приложение с ръководител, използвайки 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
постгрекс (който е адаптерът на базата данни) и екто . След като редактирате файла, стартирайте в конзолата:
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, [])
и го добавя към списъка с деца (в Elixir списъците са подобни на масиви). Определяме децата, контролирани със стратегията 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 . InvoiceItem принадлежи на Фактура и Вещ . Тази диаграма представя как нашите модели ще бъдат свързани помежду си:
Диаграмата е доста проста. Имаме маса фактури че има много invoice_items където съхраняваме всички детайли, а също и маса елементи че има много invoice_items . Можете да видите, че типът за invoice_id и item_id в invoice_items таблицата е UUID. Използваме UUID, защото помага да се замъглят маршрутите, в случай че искате да изложите приложението през API и улеснява синхронизирането, тъй като не зависи от пореден номер. Сега нека създадем таблиците, използвайки микс задачи.
Миграциите са файлове, които се използват за модифициране на схемата на базата данни. 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
s с всички дефинирани полета. Нека създадем елементи маса:
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, 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 , както ще видите след малко.
@primary_key {:id, :binary_id, autogenerate: true}
указва, че нашият първичен ключ ще бъде автоматично генериран. Тъй като използваме UUID тип, дефинираме схемата с schema 'items' do
и вътре в блока дефинираме всяко поле и взаимоотношения. Ние определихме име като низ и цена като десетична, много подобна на миграцията. След това макросът has_many :invoice_items, InvoiceItem
показва връзка между Вещ и InvoiceItem . Тъй като по конвенция ние кръстихме полето item_id в invoice_items таблица, не е необходимо да конфигурираме външния ключ. И накрая времеви марки метод ще зададе вмъкнат_ат и updated_at полета.
def changeset(data, params \ %{}) do
функция получава структура на еликсир с параметри, които ние ще тръба чрез различни функции. cast(params, @fields)
изхвърля стойностите в правилния тип. Например, можете да предавате само низове в параметрите и те ще бъдат преобразувани в правилния тип, дефиниран в схемата. 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
Това връща Ecto.Changeset
struct, която е валидна без грешки. Сега нека го запазим:
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 за краткост. В този случай връща Количка struct с всички зададени стойности, Можете да видите това вмъкнат_ат и updated_at съдържат техните времеви марки и документ за самоличност полето има UUID стойност. Нека да видим някои други случаи:
iex(3)> item2 = Cart.Item.changeset(%Cart.Item{price: Decimal.new(20)}, %{name: 'Scissors'}) #Ecto.Changeset 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
За да прекратите конзолата, натиснете два пъти 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.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
Този набор от промени е по-голям, но вече трябва да сте запознати с повечето от тях. Тук belongs_to :invoice, Cart.Invoice, type: :binary_id
определя връзката „принадлежи към“ с Количка, фактура промени, които скоро ще създадем. Следващият belongs_to :item
създава връзка с таблицата с елементи. Определихме @zero Decimal.new(0)
. В такъв случай, @ нула е като константа, която може да бъде достъпна вътре в модула. Функцията за промяна има нови части, една от които е foreign_key_constraint(:invoice_id, message: 'Select a valid invoice')
. Това ще позволи да се генерира съобщение за грешка, вместо да се генерира изключение, когато ограничението не е изпълнено. И накрая, методът set_subtotal ще изчисли междинната сума. Предаваме набора от промени и връщаме нов набор от промени с изчислената междинна сума, ако имаме както цената, така и количеството.
Сега нека създадем Количка, фактура . Така че създайте и редактирайте файла 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
Количка, фактура changeset има някои разлики. Първият е вътре схеми : has_many :invoice_items, InvoiceItem, on_delete: :delete_all
означава, че когато изтрием фактура, всички свързани с нея invoice_items ще бъдат изтрити. Имайте предвид обаче, че това не е ограничение, дефинирано в базата данни.
Нека опитаме метода create в конзолата, за да разберем нещата по-добре. Може да сте създали елементите („Хартия“, „Ножици“), които ще използваме тук:
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
съответно с първия и втория 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})
Фактурата е създадена със своите 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 което ще ни помогне да отправяме запитвания към базата данни, но първо се нуждаем от повече данни, за да обясним по-добре.
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} '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
params, за да създадете фактура, но би било по-добре просто да предадете идентификатора на артикула и цената да се попълни автоматично. Ще направим промени в Количка, фактура модул за постигане на това:
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
Първото нещо, което ще забележите, е, че сме добавили 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 фактури; първият с 2 артикула, вторият с 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
параметър. Сега да обясним items_by :
from i in Item
, този макрос избира модула Itemjoin: 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))}
, генерираме карта с всички полета, които искаме, първо избираме идентификатора и името от Item и правим операторна сума. Полето (тип ii, ^) използва полето на макроса за динамичен достъп до полеgroup_by: i.id
, Групираме по items.idorder_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 docs . С Ecto можете да създавате едновременни, устойчиви на грешки приложения с малко усилия, които могат лесно да се мащабират благодарение на виртуалната машина Erlang. Ecto осигурява основата за съхранение във вашите приложения Elixir и предоставя функции и макроси за лесно управление на вашите данни.
В този урок разгледахме Ecto.Schema , Ecto.Changeset , Ecto.Migration , Ecto.Query , и Ecto.Repo . Всеки от тези модули ви помага в различни части на вашето приложение и прави кода по-ясен и по-лесен за поддръжка и разбиране.
Ако искате да проверите кода на урока, можете да го намерите тук на GitHub.
Ако ви е харесал този урок и се интересувате от повече информация, бих препоръчал Феникс (за списък на страхотни проекти), Страхотен еликсир , и тази беседа който сравнява ActiveRecord с Ecto.