認識 Ecto,用於並發 Elixir 應用程序的不折不扣的數據庫包裝器
已發表: 2022-03-11Ecto 是一種特定領域的語言,用於編寫查詢並與 Elixir 語言中的數據庫交互。 最新版本 (2.0) 支持 PostgreSQL 和 MySQL。 (未來將提供對 MSSQL、SQLite 和 MongoDB 的支持)。 如果您是 Elixir 的新手或者對它沒有什麼經驗,我建議您閱讀 Kleber Virgilio Correia 的 Elixir 編程語言入門。
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
這個命令創建了數據庫,我們基本上已經完成了配置。 我們現在準備開始編碼,但讓我們先定義應用程序的範圍。
使用內聯項目構建發票
對於我們的演示應用程序,我們將構建一個簡單的發票工具。 對於變更集(模型),我們將有Invoice 、 Item和InvoiceItem 。 InvoiceItem屬於Invoice和Item 。 此圖表示我們的模型將如何相互關聯:
該圖非常簡單。 我們有一個包含許多invoice_items的表invoices ,我們在其中存儲所有詳細信息,還有一個包含許多invoice_items的表項目。 您可以看到invoice_items表中invoice_id和item_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_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)
。 這會在引用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.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之間的關係。 由於按照慣例我們在invoice_items表中將字段item_id命名為,因此我們不需要配置外鍵。 最後, timestamps方法將設置inserted_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)
. 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變更集有一些差異。 第一個在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.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)
外查詢
到目前為止,我們已經展示瞭如何使用關係創建新項目和新發票。 但是查詢呢? 好吧,讓我向您介紹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.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
中添加價格。
插入更多發票
你還記得,之前我們創建了一個映射項目,它使我們能夠使用項目名稱訪問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 的演講。