Découvrez Ecto, un wrapper de base de données sans compromis pour les applications Elixir simultanées
Publié: 2022-03-11Ecto est un langage spécifique à un domaine pour écrire des requêtes et interagir avec des bases de données dans le langage Elixir. La dernière version (2.0) prend en charge PostgreSQL et MySQL. (la prise en charge de MSSQL, SQLite et MongoDB sera disponible à l'avenir). Si vous êtes nouveau sur Elixir ou si vous avez peu d'expérience avec Elixir, je vous recommande de lire Premiers pas avec le langage de programmation Elixir de Kleber Virgilio Correia.
Ecto est composé de quatre composants principaux :
- Ecto.Repo. Définit les référentiels qui sont des wrappers autour d'un magasin de données. En l'utilisant, nous pouvons insérer, créer, supprimer et interroger un dépôt. Un adaptateur et des informations d'identification sont nécessaires pour communiquer avec la base de données.
- Ecto.Schema. Les schémas sont utilisés pour mapper n'importe quelle source de données dans une structure Elixir.
- Ecto.Changeset. Les ensembles de modifications permettent aux développeurs de filtrer et de diffuser des paramètres externes, ainsi qu'un mécanisme pour suivre et valider les modifications avant qu'elles ne soient appliquées aux données.
- Ecto.Query. Fournit une requête SQL de type DSL pour récupérer des informations à partir d'un référentiel. Les requêtes dans Ecto sont sécurisées, évitant les problèmes courants tels que l'injection SQL, tout en restant composables, ce qui permet aux développeurs de créer des requêtes pièce par pièce au lieu de toutes en même temps.
Pour ce tutoriel, vous aurez besoin de :
- Elixir installé (guide d'installation pour 1.2 ou version ultérieure)
- PostgreSQL installé
- Un utilisateur défini avec l'autorisation de créer une base de données (Remarque : Nous utiliserons l'utilisateur « postgres » avec le mot de passe « postgres » comme exemple tout au long de ce didacticiel.)
Installation et configuration
Pour commencer, créons une nouvelle application avec un superviseur à l'aide de Mix. Mix est un outil de construction fourni avec Elixir qui fournit des tâches pour créer, compiler, tester votre application, gérer ses dépendances et bien plus encore.
mix new cart --sup
Cela créera un répertoire cart avec les fichiers de projet initiaux :
* 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
Nous utilisons l'option --sup
car nous avons besoin d'un arbre de supervision qui maintiendra la connexion à la base de données. Ensuite, nous allons dans le répertoire cart
avec cd cart
et ouvrons le fichier mix.exs
et remplaçons son contenu :
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
Dans l' def application do
devons-nous ajouter comme applications :postgrex, :ecto
afin qu'elles puissent être utilisées dans notre application. Nous devons également les ajouter en tant que dépendances en ajoutant defp deps do
postgrex (qui est l'adaptateur de base de données) et ecto . Une fois que vous avez modifié le fichier, exécutez dans la console :
mix deps.get
Cela installera toutes les dépendances et créera un fichier mix.lock
qui stocke toutes les dépendances et sous-dépendances des packages installés (similaire à Gemfile.lock
dans bundler).
Ecto.Repo
Nous allons maintenant voir comment définir un repo dans notre application. Nous pouvons avoir plusieurs référentiels, ce qui signifie que nous pouvons nous connecter à plusieurs bases de données. Il faut configurer la base de données dans le fichier config/config.exs
:
use Mix.Config config :cart, ecto_repos: [Cart.Repo]
Nous ne faisons que définir le minimum, afin que nous puissions exécuter la commande suivante. Avec la ligne :cart, cart_repos: [Cart.Repo]
nous indiquons à Ecto quels dépôts nous utilisons. C'est une fonctionnalité intéressante car elle nous permet d'avoir de nombreux dépôts, c'est-à-dire que nous pouvons nous connecter à plusieurs bases de données.
Exécutez maintenant la commande suivante :
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]
Cette commande génère le référentiel. Si vous lisez la sortie, elle vous demande d'ajouter un superviseur et un référentiel dans votre application. Commençons par le superviseur. Nous allons éditer 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
Dans ce fichier, nous définissons le superviseur supervisor(Cart.Repo, [])
et l'ajoutons à la liste des enfants (dans Elixir, les listes sont similaires aux tableaux). Nous définissons les enfants supervisés avec la stratégie strategy: :one_for_one
ce qui signifie que, si l'un des processus supervisés échoue, le superviseur ne redémarrera que ce processus dans son état par défaut. Vous pouvez en savoir plus sur les superviseurs ici. Si vous regardez lib/cart/repo.ex
, vous verrez que ce fichier a déjà été créé, ce qui signifie que nous avons un Repo pour notre application.
defmodule Cart.Repo do use Ecto.Repo, otp_app: :cart end
Modifions maintenant le fichier de configuration 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"
Après avoir défini toute la configuration de notre base de données, nous pouvons maintenant la générer en exécutant :
mix ecto.create
Cette commande crée la base de données et, avec cela, nous avons essentiellement terminé la configuration. Nous sommes maintenant prêts à commencer à coder, mais définissons d'abord la portée de notre application.
Créer une facture avec des éléments en ligne
Pour notre application de démonstration, nous allons créer un outil de facturation simple. Pour les changesets (modèles), nous aurons Invoice , Item et InvoiceItem . InvoiceItem appartient à Invoice et Item . Ce diagramme représente comment nos modèles seront liés les uns aux autres :
Le schéma est assez simple. Nous avons une table de factures contenant de nombreux éléments de facture où nous stockons tous les détails, ainsi qu'une table d' éléments contenant de nombreux éléments de facture . Vous pouvez voir que le type de l'ID de facture et de l'ID de l'article dans la table des articles de facture est UUID. Nous utilisons UUID car il aide à masquer les itinéraires, au cas où vous voudriez exposer l'application via une API et simplifie la synchronisation puisque vous ne dépendez pas d'un numéro séquentiel. Créons maintenant les tables à l'aide des tâches Mix.
Ecto.Migration
Les migrations sont des fichiers utilisés pour modifier le schéma de la base de données. Ecto.Migration vous offre un ensemble de méthodes pour créer des tables, ajouter des index, créer des contraintes et d'autres éléments liés au schéma. Les migrations aident vraiment à maintenir la synchronisation de l'application avec la base de données. Créons un script de migration pour notre première table :
mix ecto.gen.migration create_invoices
Cela générera un fichier similaire à priv/repo/migrations/20160614115844_create_invoices.exs
où nous définirons notre migration. Ouvrez le fichier généré et modifiez son contenu comme suit :
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
À l'intérieur de la méthode def change do
nous le schéma qui générera le SQL pour la base de données. create table(:invoices, primary_key: false) do
créera la table des factures . Nous avons défini primary_key: false
mais nous ajouterons un champ ID de type UUID, un champ client de type texte, un champ date de type date. La méthode timestamps
générera les champs inserted_at
et updated_at
remplit automatiquement avec l'heure à laquelle l'enregistrement a été inséré et l'heure à laquelle il a été mis à jour, respectivement. Accédez maintenant à la console et exécutez la migration :
mix ecto.migrate
Nous avons créé le tableau invoice
s avec tous les champs définis. Créons la table des éléments :
mix ecto.gen.migration create_items
Modifiez maintenant le script de migration généré :
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
La nouveauté ici est le champ décimal qui autorise les nombres à 12 chiffres, dont 2 pour la partie décimale du nombre. Exécutons à nouveau la migration :
mix ecto.migrate
Nous avons maintenant créé la table items et créons enfin la table bill_items :
mix ecto.gen.migration create_invoice_items
Modifiez la 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
Comme vous pouvez le voir, cette migration comporte de nouvelles parties. La première chose que vous remarquerez est add :invoice_id, references(:invoices, type: :uuid, null: false)
. Cela crée le champ id_facture avec une contrainte dans la base de données qui référence la table des factures . Nous avons le même modèle pour le champ item_id . Une autre chose qui est différente est la façon dont nous créons un index : create index(:invoice_items, [:invoice_id])
crée l'index facture_items_invoice_id_index .
Ecto.Schema et Ecto.Changeset
Dans Ecto, Ecto.Model
a été déprécié en faveur de l'utilisation d' Ecto.Schema
, nous appellerons donc les modules schémas au lieu de modèles. Créons les changesets. Nous allons commencer par l'Item le plus simple du changeset et créer le fichier 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
En haut, nous injectons du code dans le changeset en utilisant use Ecto.Schema
. Nous utilisons également import Ecto.Changeset
pour importer des fonctionnalités depuis Ecto.Changeset . Nous aurions pu spécifier les méthodes spécifiques à importer, mais restons simples. L' alias Cart.InvoiceItem
nous permet d'écrire directement à l'intérieur du changeset InvoiceItem , comme vous le verrez dans un instant.
Ecto.Schéma
La @primary_key {:id, :binary_id, autogenerate: true}
spécifie que notre clé primaire sera générée automatiquement. Puisque nous utilisons un type UUID, nous définissons le schéma avec schema "items" do
de schéma et à l'intérieur du bloc, nous définissons chaque champ et relations. Nous avons défini le nom comme une chaîne et le prix comme un nombre décimal, très similaire à la migration. Ensuite, la macro has_many :invoice_items, InvoiceItem
indique une relation entre Item et InvoiceItem . Puisque par convention nous avons nommé le champ item_id dans la table de la facture_items , nous n'avons pas besoin de configurer la clé étrangère. Enfin, la méthode des horodatages définira les champs insert_at et updated_at .
Ecto.Changeset
La def changeset(data, params \\ %{}) do
reçoit une structure Elixir avec des paramètres que nous dirigerons à travers différentes fonctions. cast(params, @fields)
les valeurs dans le type correct. Par exemple, vous ne pouvez transmettre que des chaînes dans les paramètres et celles-ci seront converties dans le type correct défini dans le schéma. validate_required([:name, :price])
valide que les champs name et price sont présents, validate_number(:price, greater_than_or_equal_to: Decimal.new(0))
valide que le nombre est supérieur ou égal à 0 ou dans ce cas Decimal.new(0)
.
C'était beaucoup à assimiler, alors regardons cela dans la console avec des exemples afin que vous puissiez mieux comprendre les concepts :
iex -S mix
Cela chargera la console. -S mix
charge le projet en cours dans le REPL iex.
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>
Cela renvoie une structure Ecto.Changeset
qui est valide sans erreur. Maintenant, sauvegardons-le :
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>}
Nous ne montrons pas le SQL par souci de brièveté. Dans ce cas, il renvoie la structure Cart.Item avec toutes les valeurs définies. Vous pouvez voir que insert_at et updated_at contiennent leurs horodatages et que le champ id a une valeur UUID. Voyons d'autres cas :

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)
Nous avons maintenant défini l'élément Scissors
d'une manière différente, en définissant le prix directement %Cart.Item{price: Decimal.new(20)}
. Nous devons définir son type correct, contrairement au premier élément où nous venons de passer une chaîne comme prix. Nous aurions pu passer un flottant et cela aurait été converti en un type décimal. Si nous passons, par exemple %Cart.Item{price: 12.5}
, lorsque vous insérez l'élément, une exception est émise indiquant que le type ne correspond pas.
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>
Pour fermer la console, appuyez deux fois sur Ctrl+C. Vous pouvez voir que les validations fonctionnent et que le prix doit être supérieur ou égal à zéro (0). Comme vous pouvez le voir, nous avons défini tout le schéma Ecto.Schema qui est la partie liée à la façon dont la structure du module est définie et le changeset Ecto.Changeset qui est toutes les validations et le casting. Continuons et créons le fichier 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
Cet ensemble de modifications est plus volumineux, mais vous devriez déjà en connaître la majeure partie. Ici belongs_to :invoice, Cart.Invoice, type: :binary_id
définit la relation "appartient à" avec l'ensemble de modifications Cart.Invoice que nous allons bientôt créer. Le prochain belongs_to :item
crée une relation avec la table items. Nous avons défini @zero Decimal.new(0)
. Dans ce cas, @zero est comme une constante accessible à l'intérieur du module. La fonction changeset a de nouvelles parties, dont l'une est foreign_key_constraint(:invoice_id, message: "Select a valid invoice")
. Cela permettra de générer un message d'erreur au lieu de générer une exception lorsque la contrainte n'est pas remplie. Et enfin, la méthode set_subtotal calculera le sous-total. Nous passons le changeset et renvoyons un nouveau changeset avec le sous-total calculé si nous avons à la fois le prix et la quantité.
Maintenant, créons le Cart.Invoice . Créez et éditez donc le fichier lib/cart/invoice.ex
pour qu'il contienne les éléments suivants :
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
Le jeu de modifications Cart.Invoice présente quelques différences. Le premier est à l'intérieur des schémas : has_many :invoice_items, InvoiceItem, on_delete: :delete_all
signifie que lorsque nous supprimons une facture, tous les éléments de facture associés seront supprimés. Gardez à l'esprit, cependant, qu'il ne s'agit pas d'une contrainte définie dans la base de données.
Essayons la méthode create dans la console pour mieux comprendre les choses. Vous avez peut-être créé les éléments ("Papier", "Ciseaux") que nous utiliserons ici :
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) }
Nous avons récupéré tous les éléments avec Cart.Repo.all et avec la fonction Enum.map nous obtenons simplement l' item.id
de chaque élément. Dans la deuxième ligne, nous attribuons simplement id1
et id2
avec les premier et deuxième items_ids, respectivement :
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})
La facture a été créée avec ses éléments de facture et nous pouvons maintenant récupérer toutes les factures.
iex(4)> alias Cart.{Repo, Invoice} iex(5)> Repo.all(Invoice)
Vous pouvez voir qu'il renvoie la facture mais nous aimerions également voir les éléments de facture :
iex(6)> Repo.all(Invoice) |> Repo.preload(:invoice_items)
Avec la fonction Repo.preload , nous pouvons obtenir les éléments de invoice_items
. Notez que cela peut traiter des requêtes simultanément. Dans mon cas, la requête ressemblait à ceci:
iex(7)> Repo.get(Invoice, "5d573153-b3d6-46bc-a2c0-6681102dd3ab") |> Repo.preload(:invoice_items)
Ecto.Query
Jusqu'à présent, nous avons montré comment créer de nouveaux éléments et de nouvelles factures avec des relations. Mais qu'en est-il de l'interrogation ? Eh bien, laissez-moi vous présenter Ecto.Query qui nous aidera à interroger la base de données, mais nous avons d'abord besoin de plus de données pour mieux expliquer.
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")})
Nous devrions maintenant avoir 8 éléments et il y a un "Chocolate" répété. Nous voudrons peut-être savoir quels éléments sont répétés. Essayons donc cette requête :
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"}]
Vous pouvez voir que dans la requête, nous voulions renvoyer une carte avec le nom de l'élément et le nombre de fois qu'il apparaît dans la table des éléments. Alternativement, cependant, nous pourrions plus probablement être intéressés à voir quels sont les produits les plus vendus. Alors pour cela, créons quelques factures. Tout d'abord, simplifions-nous la vie en créant une carte pour accéder à un 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"}
Comme vous pouvez le voir, nous avons créé une carte en utilisant une compréhension
iex(12)> line_items = [%{item_id: items["Chocolates"], quantity: 2}]
Nous devons ajouter le prix dans les paramètres de la invoice_items
pour créer une facture, mais il serait préférable de simplement transmettre l'identifiant de l'article et que le prix soit rempli automatiquement. Nous apporterons des modifications au module Cart.Invoice pour y parvenir :
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
La première chose que vous remarquerez est que nous avons ajouté Ecto.Query , qui nous permettra d'interroger la base de données. La nouvelle fonction est defp items_with_prices(items) do
qui recherche parmi les articles et trouve et fixe le prix de chaque article.
Tout d'abord, defp items_with_prices(items) do
reçoit une liste en argument. Avec item_ids = Enum.map(items, fn(item) -> item[:item_id] || item["item_id"] end)
, nous parcourons tous les éléments et n'obtenons que l' item_id . Comme vous pouvez le voir, nous accédons soit avec atom :item_id
soit avec la chaîne "item_id", puisque les cartes peuvent avoir l'une ou l'autre comme clés. La requête q = from(i in Item, select: %{id: i.id, price: i.price}, where: i.id in ^item_ids)
trouvera tous les éléments qui sont dans item_ids
et renverra une carte avec item.id
et item.price
. On peut alors exécuter la requête prices = Repo.all(q)
qui renvoie une liste de cartes. Nous devons ensuite parcourir les articles et créer une nouvelle liste qui ajoutera le prix. Enum.map(items, fn(item) ->
parcourt chaque article, trouve le prix Enum.find(prices, fn(p) -> p[:id] == item_id end)[:price] || 0
, et crée une nouvelle liste avec item_id
, quantité et prix. Et avec cela, il n'est plus nécessaire d'ajouter le prix dans chacun des éléments de invoice_items
.
Insérer plus de factures
Comme vous vous en souvenez, nous avons précédemment créé un élément de carte qui nous permet d'accéder à l' identifiant en utilisant le nom de l'élément pour, par exemple items["Gum"]
"cb1c5a93-ecbf-4e4b-8588-cc40f7d12364". Cela simplifie la création de la facture_items . Créons plus de factures. Redémarrez la console et exécutez :
Iex -S mix
iex(1)> Repo.delete_all(InvoiceItem); Repo.delete_all(Invoice)
Nous supprimons tous les éléments de facture et les factures pour avoir une ardoise vierge :
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})
Nous avons maintenant 3 factures ; le premier avec 2 éléments, le second avec 3 éléments et le troisième avec 6 éléments. Nous aimerions maintenant savoir quels produits sont les articles les plus vendus ? Pour répondre à cela, nous allons créer une requête pour trouver les articles les plus vendus par quantité et par sous-total (prix x quantité).
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
Nous importons Ecto.Query puis nous alias Cart.{InvoiceItem, Item, Repo}
donc nous n'avons pas besoin d'ajouter Cart au début de chaque module. La première fonction items_by_quantity appelle la fonction items_by
, en passant le paramètre :quantity
et en appelant Repo.all pour exécuter la requête. La fonction items_by_subtotal est similaire à la fonction précédente mais passe le paramètre :subtotal
. Expliquons maintenant items_by :
-
from i in Item
, cette macro sélectionne le module Item -
join: ii in InvoiceItem, on: ii.item_id == i.id
, crée une jointure sur la condition "items.id = facture_items.item_id" -
select: %{id: i.id, name: i.name, total: sum(field(ii, ^type))}
, nous générons une carte avec tous les champs que nous voulons d'abord nous sélectionnons l'id et le nom de Item et on fait une somme d'opérateur. Le champ(ii, ^type) utilise le champ macro pour accéder dynamiquement à un champ -
group_by: i.id
, Nous regroupons par items.id -
order_by: [desc: sum(field(ii, ^type))]
et enfin trier par la somme dans l'ordre décroissant
Jusqu'à présent, nous avons écrit la requête dans le style liste mais nous pourrions la réécrire dans le style macro :
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
Je préfère écrire les requêtes sous forme de liste car je trouve cela plus lisible.
Conclusion
Nous avons couvert une bonne partie de ce que vous pouvez faire dans une application avec Ecto. Bien sûr, vous pouvez apprendre beaucoup plus de la documentation Ecto. Avec Ecto, vous pouvez créer des applications concurrentes tolérantes aux pannes avec peu d'effort qui peuvent évoluer facilement grâce à la machine virtuelle Erlang. Ecto fournit la base du stockage dans vos applications Elixir et fournit des fonctions et des macros pour gérer facilement vos données.
Dans ce didacticiel, nous avons examiné Ecto.Schema, Ecto.Changeset, Ecto.Migration, Ecto.Query et Ecto.Repo. Chacun de ces modules vous aide dans différentes parties de votre application et rend le code plus explicite et plus facile à maintenir et à comprendre.
Si vous souhaitez consulter le code du didacticiel, vous pouvez le trouver ici sur GitHub.
Si vous avez aimé ce tutoriel et que vous êtes intéressé par plus d'informations, je recommanderais Phoenix (pour une liste de projets géniaux), Awesome Elixir et cette conférence qui compare ActiveRecord à Ecto.