Lernen Sie Ecto kennen, den kompromisslosen Datenbank-Wrapper für gleichzeitige Elixir-Apps
Veröffentlicht: 2022-03-11Ecto 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.
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)
.
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.