认识 Ecto,用于并发 Elixir 应用程序的不折不扣的数据库包装器

已发表: 2022-03-11

Ecto 是一种特定领域的语言,用于编写查询并与 Elixir 语言中的数据库交互。 最新版本 (2.0) 支持 PostgreSQL 和 MySQL。 (未来将提供对 MSSQL、SQLite 和 MongoDB 的支持)。 如果您是 Elixir 的新手或者对它没有什么经验,我建议您阅读 Kleber Virgilio Correia 的 Elixir 编程语言入门。

厌倦了所有的 SQL 方言? 通过 Ecto 与您的数据库对话。
鸣叫

Ecto 由四个主要组件组成:

  • Ecto.Repo。 定义作为数据存储包装器的存储库。 使用它,我们可以插入、创建、删除和查询 repo。 与数据库通信需要适配器和凭据。
  • Ecto.Schema。 Schemas 用于将任何数据源映射到 Elixir 结构中。
  • Ecto.Changeset。 变更集为开发人员提供了一种过滤和强制转换外部参数的方法,以及一种在将更改应用于数据之前跟踪和验证更改的机制。
  • 外查询。 提供类似 DSL 的 SQL 查询,用于从存储库中检索信息。 Ecto 中的查询是安全的,避免了 SQL 注入等常见问题,同时仍然是可组合的,允许开发人员逐段构建查询,而不是一次全部构建。

对于本教程,您将需要:

  • Elixir 已安装(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选项,因为我们需要一个主管树来保持与数据库的连接。 接下来,我们用cd cart进入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来存储已安装包的所有依赖项和子依赖项(类似于 bundler 中的Gemfile.lock )。

外回购

我们现在将看看如何在我们的应用程序中定义一个 repo。 我们可以拥有多个 repo,这意味着我们可以连接到多个数据库。 我们需要在config/config.exs文件中配置数据库:

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

我们只是设置最小值,所以我们可以运行下一个命令。 通过:cart, cart_repos: [Cart.Repo]我们告诉 Ecto 我们正在使用哪个 repos。 这是一个很酷的功能,因为它允许我们拥有许多存储库,即我们可以连接到多个数据库。

现在运行以下命令:

 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]

此命令生成 repo。 如果您阅读输出,它会告诉您在应用程序中添加主管和 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 ,您会看到该文件已经创建,这意味着我们有一个用于我们的应用程序的Repo

 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

这个命令创建了数据库,我们基本上已经完成了配置。 我们现在准备开始编码,但让我们先定义应用程序的范围。

使用内联项目构建发票

对于我们的演示应用程序,我们将构建一个简单的发票工具。 对于变更集(模型),我们将有InvoiceItemInvoiceItemInvoiceItem属于InvoiceItem 。 此图表示我们的模型将如何相互关联:

该图非常简单。 我们有一个包含许多invoice_items的表invoices ,我们在其中存储所有详细信息,还有一个包含许多invoice_items的表项目。 您可以看到invoice_items表中invoice_iditem_id的类型为 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 类型的 ID 字段、文本类型的客户字段、日期类型的日期字段。 timestamps方法将生成字段inserted_atupdated_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) 。 这会在引用invoices表的数据库中创建带有约束的字段invoice_id 。 我们对item_id字段有相同的模式。 另一个不同的是我们创建索引的方式: create index(:invoice_items, [:invoice_id])创建索引invoice_items_invoice_id_index

Ecto.Schema 和 Ecto.Changeset

Ecto.Model中,不推荐使用 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.ChangesetEcto.Changeset导入功能。 我们可以指定要导入的特定方法,但让我们保持简单。 alias Cart.InvoiceItem允许我们直接在变更集InvoiceItem中编写,稍后您将看到。

外图式

@primary_key {:id, :binary_id, autogenerate: true}指定我们的主键将自动生成。 由于我们使用的是 UUID 类型,因此我们使用schema "items" do定义模式,并在块内定义每个字段和关系。 我们将名称定义为字符串,价格定义为十进制,与迁移非常相似。 接下来,宏has_many :invoice_items, InvoiceItem表示ItemInvoiceItem之间的关系。 由于按照惯例我们在invoice_items表中将字段item_id命名为,因此我们不需要配置外键。 最后, timestamps方法将设置inserted_atupdated_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) . Decimal.new(0)

在 Elixir 中, Decimal 操作的完成方式不同,因为它们是作为结构实现的。

有很多东西需要吸收,所以让我们在控制台中通过示例来看看这个,这样你就可以更好地掌握这些概念:

 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_atupdated_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变更集有一些差异。 第一个在schemas内: 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.allEnum.map函数获取所有项目,我们只获取每个项目的item.id 在第二行中,我们只为id1id2分别分配了第一个和第二个 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)

外查询

到目前为止,我们已经展示了如何使用关系创建新项目和新发票。 但是查询呢? 好吧,让我向您介绍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参数中添加价格来创建发票,但最好只传递商品的 id 并自动填写价格。 我们将对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 。 如您所见,我们使用 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.iditem.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中添加价格。

插入更多发票

你还记得,之前我们创建了一个映射项目,它使我们能够使用项目名称访问id ,即items["Gum"] “cb1c5a93-ecbf-4e4b-8588-cc40f7d12364”。 这使得创建invoice_items变得简单。 让我们创建更多发票。 再次启动控制台并运行:

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

我们删除所有invoice_items和 invoices 以获得空白:

 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}因此我们不需要在每个模块的开头添加 Cart。 第一个函数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))} ,我们正在生成一个包含我们想要的所有字段的映射首先我们从 Item 中选择 id 和 name我们做一个运算符总和。 field(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 的演讲。