Ecto、コンカレントElixirアプリ向けの妥協のないデータベースラッパーをご覧ください

公開: 2022-03-11

Ectoは、クエリを記述し、Elixir言語でデータベースを操作するためのドメイン固有言語です。 最新バージョン(2.0)はPostgreSQLとMySQLをサポートしています。 (MSSQL、SQLite、およびMongoDBのサポートは将来利用可能になります)。 Elixirを初めて使用する場合、またはElixirの経験がほとんどない場合は、KleberVirgilioCorreiaの「Elixirプログラミング言語入門」を読むことをお勧めします。

すべてのSQL方言にうんざりしていませんか? Ectoを介してデータベースに話しかけます。
つぶやき

Ectoは、次の4つの主要コンポーネントで構成されています。

  • Ecto.Repo。 データストアのラッパーであるリポジトリを定義します。 これを使用して、リポジトリの挿入、作成、削除、およびクエリを実行できます。 データベースと通信するには、アダプターと資格情報が必要です。
  • Ecto.Schema。 スキーマは、任意のデータソースをElixir構造体にマップするために使用されます。
  • Ecto.Changeset。 チェンジセットは、開発者が外部パラメーターをフィルター処理およびキャストする方法と、データに適用される前に変更を追跡および検証するメカニズムを提供します。
  • Ecto.Query。 リポジトリから情報を取得するためのDSLのようなSQLクエリを提供します。 Ectoのクエリは安全であり、SQLインジェクションなどの一般的な問題を回避しながら、構成可能であるため、開発者はクエリを一度に作成するのではなく、1つずつ作成できます。

このチュートリアルでは、次のものが必要になります。

  • 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と同様)。

Ecto.Repo

次に、アプリケーションでリポジトリを定義する方法を見ていきます。 複数のリポジトリを持つことができます。つまり、複数のデータベースに接続できます。 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これは、監視対象プロセスの1つに障害が発生した場合、スーパーバイザーはそのプロセスのみをデフォルト状態に再起動することを意味します。 スーパーバイザーについて詳しくは、こちらをご覧ください。 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

このコマンドはデータベースを作成し、これで基本的に構成が完了しました。 これでコーディングを開始する準備が整いましたが、最初にアプリのスコープを定義しましょう。

インラインアイテムを使用した請求書の作成

デモアプリケーションでは、簡単な請求ツールを作成します。 チェンジセット(モデル)には、 InvoiceItemInvoiceItemがあります。 InvoiceItemInvoiceItemに属しています。 この図は、モデルが互いにどのように関連するかを表しています。

ダイアグラムは非常に単純です。 すべての詳細を格納する多くのinvoice_itemsを含むテーブル請求書と、多くのinvoice_itemsを含むテーブルアイテムがあります。 invoice_itemsテーブルのinvoice_iditem_idのタイプがUUIDであることがわかります。 UUIDを使用しているのは、APIを介してアプリを公開する場合にルートをわかりにくくし、連番に依存しないため同期が簡単になるためです。 次に、Mixタスクを使用してテーブルを作成しましょう。

Ecto.Migration

移行は、データベーススキーマを変更するために使用されるファイルです。 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タイプのIDフィールド、textタイプのcustomerフィールド、dateタイプのdateフィールドを追加します。 timestampsメソッドは、Ectoがレコードが挿入された時刻と更新された時刻をそれぞれ自動的に入力するinserted_atフィールドとupdated_atフィールドを生成します。 次に、コンソールに移動して移行を実行します。

 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フィールドにも同じパターンがあります。 異なるもう1つの点は、インデックスの作成方法create index(:invoice_items, [:invoice_id])は、インデックスinvoice_items_invoice_id_indexを作成します。

Ecto.SchemaおよびEcto.Changeset

Ecto.Model Ecto.Schema使用するために、Ecto.Modelは非推奨になりました。そのため、モデルではなくモジュールスキーマを呼び出します。 チェンジセットを作成しましょう。 最も単純なチェンジセットアイテムから始めて、ファイル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内に直接書き込むことができます。

Ecto.Schema

@primary_key {:id, :binary_id, autogenerate: true}は、主キーが自動生成されることを指定します。 UUIDタイプを使用しているため、 schema "items" doを使用してスキーマを定義し、ブロック内で各フィールドと関係を定義します。 移行と非常によく似て、名前を文字列、価格を10進数として定義しました。 次に、マクロhas_many :invoice_items, InvoiceItemは、 ItemInvoiceItemの間の関係を示します。 慣例により、 invoice_itemsテーブルのフィールドにitem_idという名前を付けたため、外部キーを構成する必要はありません。 最後に、 timestampsメソッドはinserted_atフィールドとupdated_atフィールドを設定します。

Ecto.Changeset

def changeset(data, params \\ %{}) do関数は、さまざまな関数をパイプ処理するparamsを含む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では、10進演算は構造体として実装されているため、異なる方法で実行されます。

それは多くのことを理解する必要があったので、概念をよりよく理解できるように、例を使用してコンソールでこれを見てみましょう。

 iex -S mix

これにより、コンソールがロードされます。 -S mixは、現在のプロジェクトをiexREPLにロードします。

 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構造体を返します。inserted_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)}に設定しました。 文字列を価格として渡した最初のアイテムとは異なり、正しいタイプを設定する必要があります。 floatを渡すことができ、これは10進型にキャストされます。 たとえば%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を2回押します。 検証が機能しており、価格がゼロ(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はモジュール内でアクセスできる定数のようなものです。 チェンジセット関数には新しい部分があり、そのうちの1つは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は、請求書を削除すると、関連するすべての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を取得するだけです。 2行目では、 id1id2にそれぞれ1番目と2番目の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

これまで、リレーションシップを使用して新しいアイテムと新しい請求書を作成する方法を示してきました。 しかし、クエリはどうですか? さて、データベースへのクエリを作成するのに役立つ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 、quantity、およびpriceを使用して新しいリストを作成します。これにより、各invoice_itemsに価格を追加する必要がなくなります。

より多くの請求書を挿入する

ご存知のように、以前に、 items["Gum"] “ cb1c5a93-ecbf-4e4b-8588-cc40f7d12364”のアイテム名を使用してIDにアクセスできるマップアイテムを作成しました。 これにより、 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つの請求書ができました。 1つ目は2アイテム、2つ目は3アイテム、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_quantityitems_by関数を呼び出し、 :quantityパラメーターを渡し、 Repo.allを呼び出してクエリを実行します。 関数items_by_subtotalは前の関数と似ていますが、 :subtotal subtotalパラメーターを渡します。 それではitems_byについて説明しましょう:

  • from i in 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))} 、最初に必要なすべてのフィールドを含むマップを生成しています。最初にアイテムからIDと名前を選択します。演算子の合計を行います。 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を比較するこのトークをお勧めします。