Встречайте Ecto, бескомпромиссную оболочку базы данных для параллельных приложений Elixir
Опубликовано: 2022-03-11Ecto — это предметно-ориентированный язык для написания запросов и взаимодействия с базами данных на языке Elixir. Последняя версия (2.0) поддерживает PostgreSQL и MySQL. (поддержка MSSQL, SQLite и MongoDB будет доступна в будущем). Если вы новичок в Elixir или имеете небольшой опыт работы с ним, я бы порекомендовал вам прочитать Kleber Virgilio Correia «Начало работы с языком программирования Elixir».
Экто состоит из четырех основных компонентов:
- Экто.Репо. Определяет репозитории, являющиеся оболочкой хранилища данных. Используя его, мы можем вставлять, создавать, удалять и запрашивать репо. Для связи с базой данных требуются адаптер и учетные данные.
- Экто.Схема. Схемы используются для сопоставления любого источника данных со структурой Elixir.
- Экто.Набор изменений. Наборы изменений предоставляют разработчикам возможность фильтровать и приводить внешние параметры, а также механизм для отслеживания и проверки изменений перед их применением к данным.
- Экто.Запрос. Предоставляет DSL-подобный SQL-запрос для извлечения информации из репозитория. Запросы в Ecto безопасны, позволяют избежать распространенных проблем, таких как SQL-инъекция, и в то же время компонуются, что позволяет разработчикам создавать запросы по частям, а не все сразу.
Для этого урока вам понадобятся:
- Эликсир установлен (руководство по установке для версии 1.2 и выше)
- PostgreSQL установлен
- Пользователь, имеющий разрешение на создание базы данных (Примечание. В качестве примера в этом руководстве мы будем использовать пользователя «postgres» с паролем «postgres».)
Установка и настройка
Для начала давайте создадим новое приложение с супервизором, используя Mix. Mix — это инструмент сборки, поставляемый с Elixir, который предоставляет задачи для создания, компиляции, тестирования вашего приложения, управления его зависимостями и многого другого.
mix new cart --sup
Это создаст корзину каталогов с исходными файлами проекта:
* creating README.md * creating .gitignore * creating mix.exs * creating config * creating config/config.exs * creating lib * creating lib/ecto_tut.ex * creating test * creating test/test_helper.exs * creating test/ecto_tut_test.exs
Мы используем параметр --sup
, так как нам нужно дерево супервизора, которое будет поддерживать соединение с базой данных. Далее идем в каталог cart
с cd cart
и открываем файл mix.exs
и заменяем его содержимое:
defmodule Cart.Mixfile do use Mix.Project def project do [app: :cart, version: "0.0.1", elixir: "~> 1.2", build_embedded: Mix.env == :prod, start_permanent: Mix.env == :prod, deps: deps] end def application do [applications: [:logger, :ecto, :postgrex], mod: {Cart, []}] end # Type "mix help deps" for more examples and options defp deps do [{:postgrex, ">= 0.11.1"}, {:ecto, "~> 2.0"}] end end
В def application do
мы должны добавить в качестве приложений :postgrex, :ecto
чтобы их можно было использовать внутри нашего приложения. Мы также должны добавить их в качестве зависимостей, добавив в defp deps do
postgrex (который является адаптером базы данных) и ecto . После того, как вы отредактировали файл, запустите в консоли:
mix deps.get
Это установит все зависимости и создаст файл mix.lock
, в котором хранятся все зависимости и подзависимости установленных пакетов (аналогично Gemfile.lock
в сборщике).
Экто.Репо
Теперь мы рассмотрим, как определить репо в нашем приложении. У нас может быть более одного репо, то есть мы можем подключаться к более чем одной базе данных. Нам нужно настроить базу данных в файле config/config.exs
:
use Mix.Config config :cart, ecto_repos: [Cart.Repo]
Мы просто устанавливаем минимум, чтобы мы могли запустить следующую команду. С помощью строки :cart, cart_repos: [Cart.Repo]
мы сообщаем Ecto, какие репозитории мы используем. Это классная функция, поскольку она позволяет нам иметь много репозиториев, то есть мы можем подключаться к нескольким базам данных.
Теперь выполните следующую команду:
mix ecto.gen.repo
==> connection Compiling 1 file (.ex) Generated connection app ==> poolboy (compile) Compiled src/poolboy_worker.erl Compiled src/poolboy_sup.erl Compiled src/poolboy.erl ==> decimal Compiling 1 file (.ex) Generated decimal app ==> db_connection Compiling 23 files (.ex) Generated db_connection app ==> postgrex Compiling 43 files (.ex) Generated postgrex app ==> ecto Compiling 68 files (.ex) Generated ecto app ==> cart * creating lib/cart * creating lib/cart/repo.ex * updating config/config.exs Don't forget to add your new repo to your supervision tree (typically in lib/cart.ex): supervisor(Cart.Repo, []) And to add it to the list of ecto repositories in your configuration files (so Ecto tasks work as expected): config :cart, ecto_repos: [Cart.Repo]
Эта команда создает репозиторий. Если вы прочитаете вывод, вам будет предложено добавить супервизор и репозиторий в ваше приложение. Начнем с куратора. Мы отредактируем lib/cart.ex
:
defmodule Cart do use Application def start(_type, _args) do import Supervisor.Spec, warn: false children = [ supervisor(Cart.Repo, []) ] opts = [strategy: :one_for_one, name: Cart.Supervisor] Supervisor.start_link(children, opts) end end
В этом файле мы определяем супервизор supervisor(Cart.Repo, [])
и добавляем его в дочерний список (в Эликсире списки похожи на массивы). Мы определяем контролируемых детей с помощью стратегии strategy: :one_for_one
, что означает, что в случае сбоя одного из контролируемых процессов супервизор перезапустит только этот процесс в состояние по умолчанию. Подробнее о супервайзерах можно узнать здесь. Если вы посмотрите на lib/cart/repo.ex
, вы увидите, что этот файл уже создан, а это значит, что у нас есть репо для нашего приложения.
defmodule Cart.Repo do use Ecto.Repo, otp_app: :cart end
Теперь давайте отредактируем файл config/config.exs
:
use Mix.Config config :cart, ecto_repos: [Cart.Repo] config :cart, Cart.Repo, adapter: Ecto.Adapters.Postgres, database: "cart_dev", username: "postgres", password: "postgres", hostname: "localhost"
Определив всю конфигурацию для нашей базы данных, теперь мы можем сгенерировать ее, запустив:
mix ecto.create
Эта команда создает базу данных, и на этом мы, по сути, закончили настройку. Теперь мы готовы начать кодирование, но давайте сначала определим область нашего приложения.
Создание счета с встроенными элементами
Для нашего демонстрационного приложения мы создадим простой инструмент для выставления счетов. Для наборов изменений (моделей) у нас будут Invoice , Item и InvoiceItem . InvoiceItem принадлежит Invoice и Item . На этой диаграмме показано, как наши модели будут связаны друг с другом:
Схема довольно проста. У нас есть таблица invoices , в которой есть много invoice_items , где мы храним все детали, а также элементы таблицы, в которой много invoice_items . Вы можете видеть, что тип invoice_id и item_id в таблице invoice_items — это UUID. Мы используем UUID, потому что он помогает запутать маршруты, если вы хотите предоставить доступ к приложению через API, и упрощает синхронизацию, поскольку вы не зависите от порядкового номера. Теперь давайте создадим таблицы, используя задачи Mix.
Экто.Миграция
Миграции — это файлы, которые используются для изменения схемы базы данных. Ecto.Migration предоставляет вам набор методов для создания таблиц, добавления индексов, создания ограничений и других вещей, связанных со схемой. Миграции действительно помогают синхронизировать приложение с базой данных. Давайте создадим скрипт миграции для нашей первой таблицы:
mix ecto.gen.migration create_invoices
Это создаст файл, похожий на priv/repo/migrations/20160614115844_create_invoices.exs
, где мы определим нашу миграцию. Откройте сгенерированный файл и измените его содержимое следующим образом:
defmodule Cart.Repo.Migrations.CreateInvoices do use Ecto.Migration def change do create table(:invoices, primary_key: false) do add :id, :uuid, primary_key: true add :customer, :text add :amount, :decimal, precision: 12, scale: 2 add :balance, :decimal, precision: 12, scale: 2 add :date, :date timestamps end end end
Внутри def change do
определения метода мы определяем схему, которая будет генерировать SQL для базы данных. create table(:invoices, primary_key: false) do
создаст таблицуinvoices . Мы установили primary_key: false
, но мы добавим поле идентификатора типа UUID, поле клиента типа текста, поле даты типа даты. Метод timestamps
сгенерирует поля inserted_at
и updated_at
, которые Ecto автоматически заполнит временем вставки записи и временем ее обновления соответственно. Теперь идем в консоль и запускаем миграцию:
mix ecto.migrate
Мы создали таблицу invoice
со всеми определенными полями. Создадим таблицу элементов :
mix ecto.gen.migration create_items
Теперь отредактируйте сгенерированный скрипт миграции:
defmodule Cart.Repo.Migrations.CreateItems do use Ecto.Migration def change do create table(:items, primary_key: false) do add :id, :uuid, primary_key: true add :name, :text add :price, :decimal, precision: 12, scale: 2 timestamps end end end
Новым здесь является десятичное поле, которое позволяет использовать числа с 12 цифрами, 2 из которых относятся к десятичной части числа. Давайте снова запустим миграцию:
mix ecto.migrate
Теперь мы создали таблицу items и, наконец, давайте создадим таблицу invoice_items :
mix ecto.gen.migration create_invoice_items
Отредактируйте миграцию:
defmodule Cart.Repo.Migrations.CreateInvoiceItems do use Ecto.Migration def change do create table(:invoice_items, primary_key: false) do add :id, :uuid, primary_key: true add :invoice_id, references(:invoices, type: :uuid, null: false) add :item_id, references(:items, type: :uuid, null: false) add :price, :decimal, precision: 12, scale: 2 add :quantity, :decimal, precision: 12, scale: 2 add :subtotal, :decimal, precision: 12, scale: 2 timestamps end create index(:invoice_items, [:invoice_id]) create index(:invoice_items, [:item_id]) end end
Как видите, в этой миграции есть несколько новых частей. Первое, что вы заметите, это add :invoice_id, references(:invoices, type: :uuid, null: false)
. Это создает поле invoice_id с ограничением в базе данных, которое ссылается на таблицу счетов . У нас тот же шаблон для поля item_id . Другое отличие заключается в том, как мы создаем индекс: create index(:invoice_items, [:invoice_id])
создает индекс invoice_items_invoice_id_index .
Ecto.Schema и Ecto.Changeset
В Ecto.Model
устарел в пользу использования Ecto.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
указывает связь между Item и InvoiceItem . Поскольку по соглашению мы назвали поле item_id в таблице invoice_items , нам не нужно настраивать внешний ключ. Наконец, метод timestamps установит поля insert_at и updated_at .
Ecto.Changeset
Функция def changeset(data, params \\ %{}) do
получает структуру Elixir с параметрами, которые мы будем передавать через различные функции. 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<action: nil, changes: %{name: "Paper", price: #Decimal<2.5>}, errors: [], data: #Cart.Item<>, valid?: true>
Это возвращает структуру Ecto.Changeset
, которая действительна без ошибок. Теперь сохраним его:
iex(1)> item = Cart.Repo.insert!(item) %Cart.Item{__meta__: #Ecto.Schema.Metadata<:loaded, "items">, id: "66ab2ab7-966d-4b11-b359-019a422328d7", inserted_at: #Ecto.DateTime<2016-06-18 16:54:54>, invoice_items: #Ecto.Association.NotLoaded<association :invoice_items is not loaded>, name: "Paper", price: #Decimal<2.5>, updated_at: #Ecto.DateTime<2016-06-18 16:54:54>}
Мы не показываем SQL для краткости. В этом случае он возвращает структуру Cart.Item со всеми установленными значениями. Вы можете видеть, что insert_at и updated_at содержат свои метки времени, а поле id имеет значение UUID. Давайте посмотрим на другие случаи:
iex(3)> item2 = Cart.Item.changeset(%Cart.Item{price: Decimal.new(20)}, %{name: "Scissors"}) #Ecto.Changeset<action: nil, changes: %{name: "Scissors"}, errors: [], data: #Cart.Item<>, valid?: true> iex(4)> Cart.Repo.insert(item2)
Теперь мы установили предмет « Scissors
» по-другому, установив цену напрямую %Cart.Item{price: Decimal.new(20)}
. Нам нужно установить его правильный тип, в отличие от первого элемента, где мы просто передали строку в качестве цены. Мы могли бы передать число с плавающей запятой, и оно было бы приведено к десятичному типу. Если мы передадим, например, %Cart.Item{price: 12.5}
, когда вы вставите элемент, будет выдано исключение о том, что тип не соответствует.

iex(4)> invalid_item = Cart.Item.changeset(%Cart.Item{}, %{name: "Scissors", price: -1.5}) #Ecto.Changeset<action: nil, changes: %{name: "Scissors", price: #Decimal<-1.5>}, errors: [price: {"must be greater than or equal to %{number}", [number: #Decimal<0>]}], data: #Cart.Item<>, valid?: false>
Чтобы закрыть консоль, дважды нажмите Ctrl+C. Вы видите, что проверки работают, и цена должна быть больше или равна нулю (0). Как видите, мы определили всю схему Ecto.Schema , которая является частью, связанной с тем, как определяется структура модуля, и набор изменений Ecto.Changeset , который включает все проверки и приведение. Продолжим и создадим файл lib/cart/invoice_item.ex
:
defmodule Cart.InvoiceItem do use Ecto.Schema import Ecto.Changeset @primary_key {:id, :binary_id, autogenerate: true} schema "invoice_items" do belongs_to :invoice, Cart.Invoice, type: :binary_id belongs_to :item, Cart.Item, type: :binary_id field :quantity, :decimal, precision: 12, scale: 2 field :price, :decimal, precision: 12, scale: 2 field :subtotal, :decimal, precision: 12, scale: 2 timestamps end @fields ~w(item_id price quantity) @zero Decimal.new(0) def changeset(data, params \\ %{}) do data |> cast(params, @fields) |> validate_required([:item_id, :price, :quantity]) |> validate_number(:price, greater_than_or_equal_to: @zero) |> validate_number(:quantity, greater_than_or_equal_to: @zero) |> foreign_key_constraint(:invoice_id, message: "Select a valid invoice") |> foreign_key_constraint(:item_id, message: "Select a valid item") |> set_subtotal end def set_subtotal(cs) do case {(cs.changes[:price] || cs.data.price), (cs.changes[:quantity] || cs.data.quantity)} do {_price, nil} -> cs {nil, _quantity} -> cs {price, quantity} -> put_change(cs, :subtotal, Decimal.mult(price, quantity)) end end end
Этот набор изменений больше, но вы уже должны быть знакомы с большей его частью. Здесь belongs_to :invoice, Cart.Invoice, type: :binary_id
определяет связь «принадлежит» с набором изменений Cart.Invoice , который мы вскоре создадим. Следующее belongs_to :item
создает связь с таблицей items. Мы определили @zero Decimal.new(0)
. В этом случае @zero похож на константу, доступ к которой можно получить внутри модуля. В функции набора изменений появились новые части, одна из которых — foreign_key_constraint(:invoice_id, message: "Select a valid invoice")
. Это позволит генерировать сообщение об ошибке вместо создания исключения, когда ограничение не выполняется. И, наконец, метод set_subtotal рассчитает промежуточный итог. Мы передаем набор изменений и возвращаем новый набор изменений с рассчитанным промежуточным итогом, если у нас есть и цена, и количество.
Теперь давайте создадим Cart.Invoice . Поэтому создайте и отредактируйте файл lib/cart/invoice.ex
, чтобы он содержал следующее:
defmodule Cart.Invoice do use Ecto.Schema import Ecto.Changeset alias Cart.{Invoice, InvoiceItem, Repo} @primary_key {:id, :binary_id, autogenerate: true} schema "invoices" do field :customer, :string field :amount, :decimal, precision: 12, scale: 2 field :balance, :decimal, precision: 12, scale: 2 field :date, Ecto.Date has_many :invoice_items, InvoiceItem, on_delete: :delete_all timestamps end @fields ~w(customer amount balance date) def changeset(data, params \\ %{}) do data |> cast(params, @fields) |> validate_required([:customer, :date]) end def create(params) do cs = changeset(%Invoice{}, params) |> validate_item_count(params) |> put_assoc(:invoice_items, get_items(params)) if cs.valid? do Repo.insert(cs) else cs end end defp get_items(params) do items = params[:invoice_items] || params["invoice_items"] Enum.map(items, fn(item)-> InvoiceItem.changeset(%InvoiceItem{}, item) end) end defp validate_item_count(cs, params) do items = params[:invoice_items] || params["invoice_items"] if Enum.count(items) <= 0 do add_error(cs, :invoice_items, "Invalid number of items") else cs end end end
Набор изменений Cart.Invoice имеет некоторые отличия. Первый находится внутри схем : has_many :invoice_items, InvoiceItem, on_delete: :delete_all
означает, что когда мы удаляем инвойс, все связанные с ним инвойс_элементы будут удалены. Имейте в виду, однако, что это не ограничение, определенное в базе данных.
Давайте попробуем метод 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_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})
Счет был создан с его invoice_items, и теперь мы можем получить все счета.
iex(4)> alias Cart.{Repo, Invoice} iex(5)> Repo.all(Invoice)
Вы можете видеть, что он возвращает 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)
Экто.Query
До сих пор мы показали, как создавать новые элементы и новые счета со связями. Но как насчет запросов? Что ж, позвольте мне представить вам Ecto.Query , который поможет нам делать запросы к базе данных, но сначала нам нужно больше данных, чтобы лучше объяснить.
iex(1)> alias Cart.{Repo, Item, Invoice, InvoiceItem} iex(2)> Repo.insert(%Item{name: "Chocolates", price: Decimal.new("5")}) iex(3)> Repo.insert(%Item{name: "Gum", price: Decimal.new("2.5")}) iex(4)> Repo.insert(%Item{name: "Milk", price: Decimal.new("1.5")}) iex(5)> Repo.insert(%Item{name: "Rice", price: Decimal.new("2")}) iex(6)> Repo.insert(%Item{name: "Chocolates", price: Decimal.new("10")})
Теперь у нас должно быть 8 предметов и есть повторяющийся «Шоколад». Мы можем захотеть узнать, какие элементы повторяются. Итак, давайте попробуем этот запрос:
iex(7)> import Ecto.Query iex(8)> q = from(i in Item, select: %{name: i.name, count: (i.name)}, group_by: i.name) iex(9)> Repo.all(q) 19:12:15.739 [debug] QUERY OK db=2.7ms SELECT i0."name", count(i0."name") FROM "items" AS i0 GROUP BY i0."name" [] [%{count: 1, name: "Scissors"}, %{count: 1, name: "Gum"}, %{count: 2, name: "Chocolates"}, %{count: 1, name: "Paper"}, %{count: 1, name: "Milk"}, %{count: 1, name: "Test"}, %{count: 1, name: "Rice"}]
Вы можете видеть, что в запросе мы хотели вернуть карту с названием элемента и количеством раз, которое он появляется в таблице элементов. В качестве альтернативы, однако, нам может быть более интересно узнать, какие продукты являются самыми продаваемыми. Итак, для этого давайте создадим несколько счетов. Во-первых, давайте упростим себе жизнь, создав карту для доступа к item_id
:
iex(10)> l = Repo.all(from(i in Item, select: {i.name, i.id})) iex(11)> items = for {k, v} <- l, into: %{}, do: {k, v} %{"Chocolates" => "8fde33d3-6e09-4926-baff-369b6d92013c", "Gum" => "cb1c5a93-ecbf-4e4b-8588-cc40f7d12364", "Milk" => "7f9da795-4d57-4b46-9b57-a40cd09cf67f", "Paper" => "66ab2ab7-966d-4b11-b359-019a422328d7", "Rice" => "ff0b14d2-1918-495e-9817-f3b08b3fa4a4", "Scissors" => "397b0bb4-2b04-46df-84d6-d7b1360b6c72", "Test" => "9f832a81-f477-4912-be2f-eac0ec4f8e8f"}
Как видите, мы создали карту, используя понимание
iex(12)> line_items = [%{item_id: items["Chocolates"], quantity: 2}]
Нам нужно добавить цену в параметры invoice_items
, чтобы создать счет, но было бы лучше просто передать идентификатор товара, и цена будет заполнена автоматически. Для этого мы внесем изменения в модуль Cart.Invoice :
defmodule Cart.Invoice do use Ecto.Schema import Ecto.Changeset import Ecto.Query # We add to query # .... # schema, changeset and create functions don't change # The new function here is items_with_prices defp get_items(params) do items = items_with_prices(params[:invoice_items] || params["invoice_items"]) Enum.map(items, fn(item)-> InvoiceItem.changeset(%InvoiceItem{}, item) end) end # new function to get item prices defp items_with_prices(items) do item_ids = Enum.map(items, fn(item) -> item[:item_id] || item["item_id"] end) q = from(i in Item, select: %{id: i.id, price: i.price}, where: i.id in ^item_ids) prices = Repo.all(q) Enum.map(items, fn(item) -> item_id = item[:item_id] || item["item_id"] %{ item_id: item_id, quantity: item[:quantity] || item["quantity"], price: Enum.find(prices, fn(p) -> p[:id] == item_id end)[:price] || 0 } end) end
Первое, что вы заметите, это то, что мы добавили Ecto.Query , что позволит нам запрашивать базу данных. Новая функция — это defp items_with_prices(items) do
которая выполняет поиск среди предметов, находит и устанавливает цену для каждого предмета.
Во-первых, defp items_with_prices(items) do
получает список в качестве аргумента. С помощью item_ids = Enum.map(items, fn(item) -> item[:item_id] || item["item_id"] end)
мы перебираем все элементы и получаем только item_id . Как видите, мы получаем доступ либо с помощью атома :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 элементами. Теперь мы хотели бы знать, какие продукты являются наиболее продаваемыми товарами? Чтобы ответить на этот вопрос, мы собираемся создать запрос, чтобы найти самые продаваемые товары по количеству и промежуточному итогу (цена x количество).
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
этот макрос выбирает модуль Item -
join: ii in InvoiceItem, on: ii.item_id == i.id
, создает соединение по условию «items.id = invoice_items.item_id» -
select: %{id: i.id, name: i.name, total: sum(field(ii, ^type))}
, мы создаем карту со всеми полями, которые нам нужны, сначала мы выбираем идентификатор и имя из элемента и мы делаем оператор суммы. Поле (ii, ^type) использует поле макроса для динамического доступа к полю. -
group_by: i.id
, Мы группируем по items.id -
order_by: [desc: sum(field(ii, ^type))]
и, наконец, упорядочить по сумме в порядке убывания
До сих пор мы писали запрос в стиле списка, но мы могли бы переписать его в стиле макроса:
defp items_by(type) do Item |> join(:inner, [i], ii in InvoiceItem, ii.item_id == i.id) |> select([i, ii], %{id: i.id, name: i.name, total: sum(field(ii, ^type))}) |> group_by([i, _], i.id) |> order_by([_, ii], [desc: sum(field(ii, ^type))]) end
Я предпочитаю писать запросы в виде списка, так как считаю его более читабельным.
Заключение
Мы рассмотрели большую часть того, что вы можете делать в приложении с Ecto. Конечно, вы можете узнать гораздо больше из документации Ecto. С помощью Ecto вы можете с минимальными усилиями создавать параллельные отказоустойчивые приложения, которые можно легко масштабировать благодаря виртуальной машине Erlang. Ecto обеспечивает основу для хранения ваших приложений Elixir и предоставляет функции и макросы для простого управления вашими данными.
В этом руководстве мы рассмотрели Ecto.Schema, Ecto.Changeset, Ecto.Migration, Ecto.Query и Ecto.Repo. Каждый из этих модулей помогает вам в различных частях вашего приложения и делает код более явным, а также более простым в обслуживании и понимании.
Если вы хотите ознакомиться с кодом учебника, вы можете найти его здесь, на GitHub.
Если вам понравился этот урок и вы заинтересованы в дополнительной информации, я бы порекомендовал Phoenix (список замечательных проектов), Awesome Elixir и этот доклад, в котором ActiveRecord сравнивается с Ecto.