Lernen Sie Ecto kennen, den kompromisslosen Datenbank-Wrapper für gleichzeitige Elixir-Apps

Veröffentlicht: 2022-03-11

Ecto ist eine domänenspezifische Sprache zum Schreiben von Abfragen und zum Interagieren mit Datenbanken in der Elixir-Sprache. Die neueste Version (2.0) unterstützt PostgreSQL und MySQL. (Unterstützung für MSSQL, SQLite und MongoDB wird in Zukunft verfügbar sein). Falls Sie Elixir noch nicht kennen oder wenig Erfahrung damit haben, würde ich Ihnen empfehlen, Kleber Virgilio Correias Getting Started with Elixir Programming Language zu lesen.

Müde von all den SQL-Dialekten? Sprechen Sie über Ecto mit Ihrer Datenbank.
Twittern

Ecto besteht aus vier Hauptkomponenten:

  • Ecto.Repo. Definiert Repositorys, die Wrapper um einen Datenspeicher sind. Damit können wir ein Repo einfügen, erstellen, löschen und abfragen. Für die Kommunikation mit der Datenbank sind ein Adapter und Anmeldeinformationen erforderlich.
  • Ecto.Schema. Schemas werden verwendet, um eine beliebige Datenquelle einer Elixir-Struktur zuzuordnen.
  • Ecto.Changeset. Changesets bieten Entwicklern die Möglichkeit, externe Parameter zu filtern und umzuwandeln, sowie einen Mechanismus zum Nachverfolgen und Validieren von Änderungen, bevor sie auf Daten angewendet werden.
  • Ecto.Query. Stellt eine DSL-ähnliche SQL-Abfrage zum Abrufen von Informationen aus einem Repository bereit. Abfragen in Ecto sind sicher und vermeiden häufige Probleme wie SQL-Injection, während sie dennoch zusammensetzbar sind, sodass Entwickler Abfragen Stück für Stück erstellen können, anstatt alle auf einmal.

Für dieses Tutorial benötigen Sie:

  • Elixir installiert (Installationsanleitung für 1.2 oder höher)
  • PostgreSQL installiert
  • Ein Benutzer mit der Berechtigung zum Erstellen einer Datenbank (Hinweis: Wir werden in diesem Tutorial den Benutzer „postgres“ mit dem Passwort „postgres“ als Beispiel verwenden.)

Installation und Konfiguration

Lassen Sie uns zunächst mit Mix eine neue App mit einem Supervisor erstellen. Mix ist ein Build-Tool, das mit Elixir geliefert wird und Aufgaben zum Erstellen, Kompilieren, Testen Ihrer Anwendung, Verwalten ihrer Abhängigkeiten und vieles mehr bereitstellt.

 mix new cart --sup

Dadurch wird ein Verzeichnis cart mit den anfänglichen Projektdateien erstellt:

 * 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

Wir verwenden die Option --sup , da wir einen Supervisor-Baum benötigen, der die Verbindung zur Datenbank aufrechterhält. Als nächstes gehen wir mit cd cart cart das Verzeichnis cart und öffnen die Datei mix.exs und ersetzen deren Inhalt:

 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

In def application do wir als Anwendungen :postgrex, :ecto damit diese in unserer Anwendung verwendet werden können. Wir müssen diese auch als Abhängigkeiten hinzufügen, defp deps do postgrex (das ist der Datenbankadapter) und ecto hinzufügen. Nachdem Sie die Datei bearbeitet haben, führen Sie in der Konsole Folgendes aus:

 mix deps.get

Dadurch werden alle Abhängigkeiten installiert und eine Datei mix.lock , die alle Abhängigkeiten und Unterabhängigkeiten der installierten Pakete speichert (ähnlich wie Gemfile.lock im Bundler).

Ecto.Repo

Wir werden uns nun ansehen, wie ein Repo in unserer Anwendung definiert wird. Wir können mehr als ein Repo haben, was bedeutet, dass wir uns mit mehr als einer Datenbank verbinden können. Wir müssen die Datenbank in der Datei config/config.exs :

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

Wir setzen nur das Minimum, damit wir den nächsten Befehl ausführen können. Mit der Zeile :cart, cart_repos: [Cart.Repo] wir Ecto mit, welche Repos wir verwenden. Dies ist ein cooles Feature, da es uns ermöglicht, viele Repos zu haben, dh wir können uns mit mehreren Datenbanken verbinden.

Führen Sie nun den folgenden Befehl aus:

 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]

Dieser Befehl generiert das Repo. Wenn Sie die Ausgabe lesen, werden Sie aufgefordert, einen Supervisor und ein Repo in Ihrer App hinzuzufügen. Beginnen wir mit dem Vorgesetzten. Wir werden lib/cart.ex bearbeiten:

 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

In dieser Datei definieren wir den Supervisor supervisor(Cart.Repo, []) und fügen ihn der Kinderliste hinzu (in Elixir ähneln Listen Arrays). Wir definieren die überwachten Kinder mit der strategy: :one_for_one , was bedeutet, dass, wenn einer der überwachten Prozesse fehlschlägt, der Supervisor nur diesen Prozess in seinen Standardzustand zurücksetzt. Hier erfahren Sie mehr über Betreuer. Wenn Sie sich lib/cart/repo.ex , werden Sie sehen, dass diese Datei bereits erstellt wurde, was bedeutet, dass wir ein Repo für unsere Anwendung haben.

 defmodule Cart.Repo do use Ecto.Repo, otp_app: :cart end

Bearbeiten wir nun die Konfigurationsdatei 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"

Nachdem wir alle Konfigurationen für unsere Datenbank definiert haben, können wir sie jetzt generieren, indem wir Folgendes ausführen:

 mix ecto.create

Dieser Befehl erstellt die Datenbank und damit haben wir die Konfiguration im Wesentlichen abgeschlossen. Wir können jetzt mit dem Programmieren beginnen, aber lassen Sie uns zuerst den Umfang unserer App definieren.

Erstellen einer Rechnung mit Inline-Artikeln

Für unsere Demoanwendung werden wir ein einfaches Rechnungstool erstellen. Für Changesets (Modelle) haben wir Invoice , Item und InvoiceItem . InvoiceItem gehört zu Invoice und Item . Dieses Diagramm stellt dar, wie unsere Modelle miteinander in Beziehung stehen:

Das Diagramm ist ziemlich einfach. Wir haben eine Tabelle " Rechnungen " mit vielen "Invoice_Items" , in der wir alle Details speichern, und auch eine Tabelle " Items " mit vielen Invoice_Items . Sie können sehen, dass der Typ für invoice_id und item_id in der billage_items- Tabelle UUID ist. Wir verwenden UUID, weil es hilft, die Routen zu verschleiern, falls Sie die App über eine API verfügbar machen möchten, und die Synchronisierung vereinfacht, da Sie nicht auf eine fortlaufende Nummer angewiesen sind. Lassen Sie uns nun die Tabellen mit Mix-Aufgaben erstellen.

Ekto.Migration

Migrationen sind Dateien, die zum Ändern des Datenbankschemas verwendet werden. Ecto.Migration bietet Ihnen eine Reihe von Methoden zum Erstellen von Tabellen, Hinzufügen von Indizes, Erstellen von Einschränkungen und anderen schemabezogenen Dingen. Migrationen tragen wirklich dazu bei, die Anwendung mit der Datenbank synchron zu halten. Lassen Sie uns ein Migrationsskript für unsere erste Tabelle erstellen:

 mix ecto.gen.migration create_invoices

Dadurch wird eine Datei ähnlich wie priv/repo/migrations/20160614115844_create_invoices.exs , in der wir unsere Migration definieren. Öffnen Sie die generierte Datei und ändern Sie ihren Inhalt wie folgt:

 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

In method def change do definieren wir das Schema, das die SQL für die Datenbank generiert. create table(:invoices, primary_key: false) do erstellt die Tabelle invoices . Wir haben primary_key: false gesetzt, aber wir werden ein ID-Feld vom Typ UUID, ein Kundenfeld vom Typ Text und ein Datumsfeld vom Typ Datum hinzufügen. Die timestamps -Methode generiert die Felder inserted_at und updated_at , die Ecto automatisch mit der Zeit füllt, zu der der Datensatz eingefügt bzw. aktualisiert wurde. Gehen Sie nun zur Konsole und führen Sie die Migration aus:

 mix ecto.migrate

Wir haben die Tabelle invoice mit allen definierten Feldern erstellt. Lassen Sie uns die Items -Tabelle erstellen:

 mix ecto.gen.migration create_items

Bearbeiten Sie nun das generierte Migrationsskript:

 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

Das Neue hier ist das Dezimalfeld, das Zahlen mit 12 Stellen erlaubt, von denen 2 für den Dezimalteil der Zahl sind. Lassen Sie uns die Migration erneut ausführen:

 mix ecto.migrate

Jetzt haben wir die Items -Tabelle erstellt und lassen Sie uns schließlich die Invoice_items -Tabelle erstellen:

 mix ecto.gen.migration create_invoice_items

Bearbeiten Sie die Migration:

 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

Wie Sie sehen können, hat diese Migration einige neue Teile. Das erste, was Sie bemerken werden, ist add :invoice_id, references(:invoices, type: :uuid, null: false) . Dadurch wird das Feld invoice_id mit einer Einschränkung in der Datenbank erstellt, die auf die Rechnungstabelle verweist. Wir haben das gleiche Muster für das Feld item_id . Ein weiterer Unterschied ist die Art und Weise, wie wir einen Index erstellen: create index(:invoice_items, [:invoice_id]) erstellt den Index invoice_items_invoice_id_index .

Ecto.Schema und Ecto.Changeset

In Ecto wurde Ecto.Model zugunsten der Verwendung von Ecto.Schema , daher werden wir die Module Schemas anstelle von Modellen nennen. Lassen Sie uns die Änderungssätze erstellen. Wir beginnen mit dem einfachsten Changeset Item und erstellen die Datei 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

Oben fügen wir mit use Ecto.Schema Code in das Changeset ein. Wir verwenden auch import Ecto.Changeset , um Funktionen aus Ecto.Changeset zu importieren. Wir hätten angeben können, welche spezifischen Methoden importiert werden sollen, aber halten wir es einfach. Der alias Cart.InvoiceItem ermöglicht es uns, direkt in das Changeset InvoiceItem zu schreiben, wie Sie gleich sehen werden.

Ecto.Schema

Der @primary_key {:id, :binary_id, autogenerate: true} gibt an, dass unser Primärschlüssel automatisch generiert wird. Da wir einen UUID-Typ verwenden, definieren wir das Schema mit schema "items" do und innerhalb des Blocks definieren wir jedes Feld und jede Beziehung. Wir haben den Namen als Zeichenfolge und den Preis als Dezimalzahl definiert, sehr ähnlich wie bei der Migration. Als nächstes gibt das Makro has_many :invoice_items, InvoiceItem eine Beziehung zwischen Item und InvoiceItem an . Da wir das Feld item_id in der Tabelle invoice_items konventionell benannt haben, müssen wir den Fremdschlüssel nicht konfigurieren. Schließlich setzt die timestamps -Methode die Felder inserted_at und updated_at .

Ecto.Changeset

Die def changeset(data, params \\ %{}) do empfängt eine Elixir-Struktur mit Parametern, die wir durch verschiedene Funktionen leiten werden. cast(params, @fields) die Werte in den richtigen Typ um. Beispielsweise können Sie nur Zeichenfolgen in den Parametern übergeben, und diese werden in den richtigen Typ konvertiert, der im Schema definiert ist. validate_required([:name, :price]) überprüft, ob die Felder name und price vorhanden sind, validate_number(:price, greater_than_or_equal_to: Decimal.new(0)) die Zahl größer oder gleich 0 oder in diesem Fall Decimal.new(0) ist Decimal.new(0) .

In Elixir werden Decimal-Operationen anders durchgeführt, da sie als Struct implementiert sind.

Das war eine Menge zu verarbeiten, also schauen wir uns das in der Konsole mit Beispielen an, damit Sie die Konzepte besser verstehen können:

 iex -S mix

Dadurch wird die Konsole geladen. -S mix lädt das aktuelle Projekt in die 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>

Dies gibt eine fehlerfrei gültige Ecto.Changeset -Struktur zurück. Jetzt speichern wir es:

 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>}

Wir zeigen die SQL der Kürze halber nicht. In diesem Fall wird die Cart.Item- Struktur mit allen festgelegten Werten zurückgegeben. Sie können sehen, dass inserted_at und updated_at ihre Zeitstempel enthalten und das ID -Feld einen UUID-Wert hat. Sehen wir uns einige andere Fälle an:

 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)

Jetzt haben wir das Item Scissors anders eingestellt, indem wir den Preis direkt %Cart.Item{price: Decimal.new(20)} . Wir müssen den korrekten Typ festlegen, im Gegensatz zum ersten Element, bei dem wir nur eine Zeichenfolge als Preis übergeben haben. Wir hätten einen Float übergeben können und dieser wäre in einen Dezimaltyp umgewandelt worden. Wenn wir beispielsweise %Cart.Item{price: 12.5} , wird beim Einfügen des Artikels eine Ausnahme ausgelöst, die besagt, dass der Typ nicht übereinstimmt.

 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>

Um die Konsole zu beenden, drücken Sie zweimal Strg+C. Sie können sehen, dass die Validierungen funktionieren und der Preis größer oder gleich null (0) sein muss. Wie Sie sehen können, haben wir das gesamte Schema Ecto.Schema definiert, das der Teil ist, der sich darauf bezieht, wie die Struktur des Moduls definiert wird, und das Änderungsset Ecto.Changeset , das alle Validierungen und Umwandlungen umfasst. Lassen Sie uns fortfahren und die Datei 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

Dieses Änderungsset ist größer, aber Sie sollten bereits mit dem größten Teil davon vertraut sein. Hier belongs_to :invoice, Cart.Invoice, type: :binary_id definiert die „gehört zu“-Beziehung mit dem Cart.Invoice- Änderungssatz, den wir bald erstellen werden. Der nächste belongs_to :item erstellt eine Beziehung mit der Items-Tabelle. Wir haben @zero Decimal.new(0) definiert. In diesem Fall ist @zero wie eine Konstante, auf die innerhalb des Moduls zugegriffen werden kann. Die Changeset-Funktion hat neue Teile, von denen einer foreign_key_constraint(:invoice_id, message: "Select a valid invoice") ist. Dadurch kann eine Fehlermeldung generiert werden, anstatt eine Ausnahme zu generieren, wenn die Einschränkung nicht erfüllt ist. Und schließlich berechnet die Methode set_subtotal die Zwischensumme. Wir übergeben den Änderungssatz und geben einen neuen Änderungssatz mit der berechneten Zwischensumme zurück, wenn wir sowohl den Preis als auch die Menge haben.

Lassen Sie uns nun Cart.Invoice erstellen. Erstellen und bearbeiten Sie also die Datei lib/cart/invoice.ex , um Folgendes zu enthalten:

 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 Changeset weist einige Unterschiede auf. Der erste befindet sich innerhalb von Schemas : has_many :invoice_items, InvoiceItem, on_delete: :delete_all bedeutet, dass beim Löschen einer Rechnung alle zugehörigen Rechnungsartikel gelöscht werden. Beachten Sie jedoch, dass dies keine in der Datenbank definierte Einschränkung ist.

Lassen Sie uns die create-Methode in der Konsole ausprobieren, um die Dinge besser zu verstehen. Möglicherweise haben Sie die Gegenstände („Papier“, „Schere“) erstellt, die wir hier verwenden werden:

 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) }

Wir haben alle Artikel mit Cart.Repo.all abgerufen und mit der Funktion Enum.map erhalten wir nur die item.id von jedem Artikel. In der zweiten Zeile weisen wir einfach id1 und id2 mit der ersten bzw. zweiten item_ids zu:

 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})

Die Rechnung wurde mit ihren bill_items erstellt und wir können jetzt alle Rechnungen abrufen.

 iex(4)> alias Cart.{Repo, Invoice} iex(5)> Repo.all(Invoice)

Sie können sehen, dass es die Rechnung zurückgibt, aber wir möchten auch die billage_items sehen:

 iex(6)> Repo.all(Invoice) |> Repo.preload(:invoice_items)

Mit der Funktion Repo.preload können wir die invoice_items . Beachten Sie, dass dies Abfragen gleichzeitig verarbeiten kann. Bei mir sah die Abfrage so aus:

 iex(7)> Repo.get(Invoice, "5d573153-b3d6-46bc-a2c0-6681102dd3ab") |> Repo.preload(:invoice_items)

Ecto.Query

Bisher haben wir gezeigt, wie Sie neue Artikel und neue Rechnungen mit Beziehungen erstellen. Aber was ist mit Abfragen? Nun, lassen Sie mich Ihnen Ecto.Query vorstellen , das uns helfen wird, Abfragen an die Datenbank zu stellen, aber zuerst brauchen wir mehr Daten, um es besser zu erklären.

 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")})

Wir sollten jetzt 8 Artikel haben und es gibt ein wiederholtes „Schokolade“. Möglicherweise möchten wir wissen, welche Elemente wiederholt werden. Versuchen wir also diese Abfrage:

 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"}]

Sie können sehen, dass wir in der Abfrage eine Karte mit dem Namen des Elements und der Häufigkeit, mit der es in der Elementtabelle erscheint, zurückgeben wollten. Alternativ könnten wir jedoch eher daran interessiert sein, zu sehen, welche die meistverkauften Produkte sind. Lassen Sie uns dafür einige Rechnungen erstellen. Machen wir uns zunächst das Leben leichter, indem wir eine Karte erstellen, um auf eine 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"}

Wie Sie sehen können, haben wir anhand eines Verständnisses eine Karte erstellt

 iex(12)> line_items = [%{item_id: items["Chocolates"], quantity: 2}]

Wir müssen den Preis in den invoice_items hinzufügen, um eine Rechnung zu erstellen, aber es wäre besser, nur die ID des Artikels zu übergeben und den Preis automatisch eintragen zu lassen. Wir werden Änderungen am Cart.Invoice -Modul vornehmen, um dies zu erreichen:

 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

Das erste, was Sie bemerken werden, ist, dass wir Ecto.Query hinzugefügt haben, mit dem wir die Datenbank abfragen können. Die neue Funktion ist defp items_with_prices(items) do , die die Artikel durchsucht und den Preis für jeden Artikel findet und festlegt.

Zunächst erhält defp items_with_prices(items) do eine Liste als Argument. Mit item_ids = Enum.map(items, fn(item) -> item[:item_id] || item["item_id"] end) durchlaufen wir alle Items und erhalten nur die item_id . Wie Sie sehen können, greifen wir entweder mit atom :item_id oder der Zeichenfolge „item_id“ zu, da Maps beides als Schlüssel haben können. Die Abfrage q = from(i in Item, select: %{id: i.id, price: i.price}, where: i.id in ^item_ids) findet alle Artikel, die sich in item_ids befinden, und gibt eine Karte mit zurück item.id und item.price . Wir können dann die Abfrage prices = Repo.all(q) , die eine Liste von Karten zurückgibt. Wir müssen dann die Artikel durchlaufen und eine neue Liste erstellen, die den Preis hinzufügt. Die Enum.map(items, fn(item) -> iteriert durch jeden Artikel, findet den Preis Enum.find(prices, fn(p) -> p[:id] == item_id end)[:price] || 0 , und erstellt eine neue Liste mit item_id , Menge und Preis. Damit ist es nicht mehr erforderlich, den Preis in jedem der invoice_items .

Weitere Rechnungen einfügen

Wie Sie sich erinnern, haben wir zuvor ein Kartenelement erstellt, das es uns ermöglicht, auf die ID zuzugreifen, indem wir den Elementnamen für dh items items["Gum"] „cb1c5a93-ecbf-4e4b-8588-cc40f7d12364“ verwenden. Dies macht es einfach, invoice_items zu erstellen. Lassen Sie uns weitere Rechnungen erstellen. Starten Sie die Konsole erneut und führen Sie Folgendes aus:

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

Wir löschen alle Rechnungspositionen und Rechnungen, um eine leere Tafel zu haben:

 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})

Jetzt haben wir 3 Rechnungen; der erste mit 2 Artikeln, der zweite mit 3 Artikeln und der dritte mit 6 Artikeln. Wir möchten nun wissen, welche Produkte die meistverkauften Artikel sind? Um dies zu beantworten, werden wir eine Abfrage erstellen, um die meistverkauften Artikel nach Menge und Zwischensumme (Preis x Menge) zu finden.

 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

Wir importieren Ecto.Query und aliasen dann alias Cart.{InvoiceItem, Item, Repo} sodass wir Cart nicht am Anfang jedes Moduls hinzufügen müssen. Die erste Funktion items_by_quantity ruft die Funktion items_by , übergibt den Parameter :quantity und ruft Repo.all auf, um die Abfrage auszuführen. Die Funktion items_by_subtotal ähnelt der vorherigen Funktion, übergibt jedoch den Parameter :subtotal . Lassen Sie uns nun items_by erklären:

  • from i in Item wählt dieses Makro das Item-Modul aus
  • join: ii in InvoiceItem, on: ii.item_id == i.id , erstellt einen Join für die Bedingung „items.id = bill_items.item_id“
  • select: %{id: i.id, name: i.name, total: sum(field(ii, ^type))} , wir generieren eine Karte mit allen gewünschten Feldern. Zuerst wählen wir die ID und den Namen aus Item und wir machen eine Operatorsumme. Das Feld (ii, ^type) verwendet das Makrofeld, um dynamisch auf ein Feld zuzugreifen
  • group_by: i.id , Wir gruppieren nach items.id
  • order_by: [desc: sum(field(ii, ^type))] und schließlich nach der Summe in absteigender Reihenfolge sortieren

Bisher haben wir die Abfrage im Listenstil geschrieben, aber wir könnten sie im Makrostil umschreiben:

 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

Ich ziehe es vor, Abfragen in Listenform zu schreiben, da ich es lesbarer finde.

Fazit

Wir haben einen guten Teil dessen abgedeckt, was Sie in einer App mit Ecto tun können. Natürlich gibt es noch viel mehr, was Sie aus den Ecto-Dokumenten lernen können. Mit Ecto erstellen Sie mit wenig Aufwand nebenläufige, fehlertolerante Anwendungen, die sich dank der Erlang Virtual Machine einfach skalieren lassen. Ecto bildet die Grundlage für die Speicherung in Ihren Elixir-Anwendungen und bietet Funktionen und Makros zur einfachen Verwaltung Ihrer Daten.

In diesem Tutorial haben wir Ecto.Schema, Ecto.Changeset, Ecto.Migration, Ecto.Query und Ecto.Repo untersucht. Jedes dieser Module hilft Ihnen in verschiedenen Teilen Ihrer Anwendung und macht den Code expliziter und leichter zu warten und zu verstehen.

Wenn Sie sich den Code des Tutorials ansehen möchten, finden Sie ihn hier auf GitHub.

Wenn Ihnen dieses Tutorial gefallen hat und Sie an weiteren Informationen interessiert sind, würde ich Phoenix (für eine Liste großartiger Projekte), Awesome Elixir und diesen Vortrag empfehlen, der ActiveRecord mit Ecto vergleicht.