Conozca a Ecto, contenedor de base de datos sin concesiones para aplicaciones Elixir simultáneas
Publicado: 2022-03-11Ecto es un lenguaje específico de dominio para escribir consultas e interactuar con bases de datos en el lenguaje Elixir. La última versión (2.0) es compatible con PostgreSQL y MySQL. (La compatibilidad con MSSQL, SQLite y MongoDB estará disponible en el futuro). En caso de que sea nuevo en Elixir o tenga poca experiencia con él, le recomendaría que lea Primeros pasos con el lenguaje de programación Elixir de Kleber Virgilio Correia.
Ecto se compone de cuatro componentes principales:
- Ecto.Repo. Define repositorios que son contenedores alrededor de un almacén de datos. Utilizándolo, podemos insertar, crear, eliminar y consultar un repositorio. Se requiere un adaptador y credenciales para comunicarse con la base de datos.
- Ecto.Esquema. Los esquemas se utilizan para mapear cualquier fuente de datos en una estructura Elixir.
- Ecto.Conjunto de cambios. Los conjuntos de cambios proporcionan una forma para que los desarrolladores filtren y emitan parámetros externos, así como un mecanismo para rastrear y validar los cambios antes de que se apliquen a los datos.
- Ecto.Consulta. Proporciona una consulta SQL similar a DSL para recuperar información de un repositorio. Las consultas en Ecto son seguras, lo que evita problemas comunes como la inyección de SQL, sin dejar de ser componible, lo que permite a los desarrolladores crear consultas pieza por pieza en lugar de todas a la vez.
Para este tutorial, necesitarás:
- Elixir instalado (guía de instalación para 1.2 o posterior)
- PostgreSQL instalado
- Un usuario definido con permiso para crear una base de datos (Nota: Usaremos el usuario "postgres" con la contraseña "postgres" como ejemplo a lo largo de este tutorial).
Instalacion y configuracion
Para empezar, creemos una nueva aplicación con un supervisor usando Mix. Mix es una herramienta de compilación que se incluye con Elixir y proporciona tareas para crear, compilar, probar su aplicación, administrar sus dependencias y mucho más.
mix new cart --sup
Esto creará un carrito de directorio con los archivos iniciales del proyecto:
* 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
Estamos usando la opción --sup
ya que necesitamos un árbol supervisor que mantendrá la conexión con la base de datos. A continuación, vamos al directorio cart
con cd cart
y abrimos el archivo mix.exs
y reemplazamos su contenido:
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
En def application do
tenemos que agregar como aplicaciones :postgrex, :ecto
para que puedan usarse dentro de nuestra aplicación. También tenemos que agregarlos como dependencias agregando defp deps do
postgrex (que es el adaptador de la base de datos) y ecto . Una vez que haya editado el archivo, ejecute en la consola:
mix deps.get
Esto instalará todas las dependencias y creará un archivo mix.lock
que almacena todas las dependencias y subdependencias de los paquetes instalados (similar a Gemfile.lock
en el paquete).
Ecto.Repo
Ahora veremos cómo definir un repositorio en nuestra aplicación. Podemos tener más de un repositorio, lo que significa que podemos conectarnos a más de una base de datos. Necesitamos configurar la base de datos en el archivo config/config.exs
:
use Mix.Config config :cart, ecto_repos: [Cart.Repo]
Solo estamos configurando el mínimo, por lo que podemos ejecutar el siguiente comando. Con la línea :cart, cart_repos: [Cart.Repo]
le estamos diciendo a Ecto qué repositorios estamos usando. Esta es una característica interesante ya que nos permite tener muchos repositorios, es decir, podemos conectarnos a múltiples bases de datos.
Ahora ejecuta el siguiente comando:
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]
Este comando genera el repositorio. Si lee el resultado, le indica que agregue un supervisor y un repositorio en su aplicación. Comencemos con el supervisor. 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
En este archivo, estamos definiendo el supervisor supervisor(Cart.Repo, [])
y agregándolo a la lista de niños (en Elixir, las listas son similares a los arreglos). Definimos los niños supervisados con la estrategia strategy: :one_for_one
lo que significa que, si uno de los procesos supervisados falla, el supervisor reiniciará solo ese proceso a su estado predeterminado. Puede obtener más información sobre los supervisores aquí. Si observa lib/cart/repo.ex
, verá que este archivo ya se ha creado, lo que significa que tenemos un repositorio para nuestra aplicación.
defmodule Cart.Repo do use Ecto.Repo, otp_app: :cart end
Ahora editemos el archivo de configuración 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"
Habiendo definido toda la configuración para nuestra base de datos, ahora podemos generarla ejecutando:
mix ecto.create
Este comando crea la base de datos y, con eso, esencialmente hemos terminado la configuración. Ahora estamos listos para comenzar a codificar, pero primero definamos el alcance de nuestra aplicación.
Creación de una factura con artículos en línea
Para nuestra aplicación de demostración, crearemos una herramienta de facturación simple. Para conjuntos de cambios (modelos) tendremos Invoice , Item y InvoiceItem . InvoiceItem pertenece a Factura y Artículo . Este diagrama representa cómo se relacionarán nuestros modelos entre sí:
El diagrama es bastante simple. Tenemos una tabla de facturas que tiene muchos elementos de factura donde almacenamos todos los detalles y también una tabla de elementos que tiene muchos elementos de factura . Puede ver que el tipo de id_factura y id_artículo en la tabla artículos_factura es UUID. Usamos UUID porque ayuda a ofuscar las rutas, en caso de que desee exponer la aplicación a través de una API y simplifica la sincronización, ya que no depende de un número secuencial. Ahora vamos a crear las tablas usando tareas Mix.
Ecto.Migración
Las migraciones son archivos que se utilizan para modificar el esquema de la base de datos. Ecto.Migration le brinda un conjunto de métodos para crear tablas, agregar índices, crear restricciones y otras cosas relacionadas con el esquema. Las migraciones realmente ayudan a mantener la aplicación sincronizada con la base de datos. Vamos a crear un script de migración para nuestra primera tabla:
mix ecto.gen.migration create_invoices
Esto generará un archivo similar a priv/repo/migrations/20160614115844_create_invoices.exs
donde definiremos nuestra migración. Abra el archivo generado y modifique su contenido para que quede de la siguiente manera:
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
Dentro def change do
, definimos el esquema que generará el SQL para la base de datos. create table(:invoices, primary_key: false) do
creará la tabla de facturas . Hemos establecido la primary_key: false
, pero agregaremos un campo de ID de tipo UUID, un campo de cliente de tipo texto, un campo de fecha de tipo fecha. El método de sellos de timestamps
generará los campos inserted_at
las y updated_at
que Ecto llena automáticamente con la hora en que se insertó el registro y la hora en que se actualizó, respectivamente. Ahora ve a la consola y ejecuta la migración:
mix ecto.migrate
Hemos creado la tabla de invoice
con todos los campos definidos. Vamos a crear la tabla de elementos :
mix ecto.gen.migration create_items
Ahora edite el script de migración generado:
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
Lo nuevo aquí es el campo decimal que permite números con 12 dígitos, 2 de los cuales son para la parte decimal del número. Ejecutemos la migración de nuevo:
mix ecto.migrate
Ahora hemos creado la tabla de elementos y finalmente vamos a crear la tabla de elementos de factura :
mix ecto.gen.migration create_invoice_items
Edite la migración:
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
Como puede ver, esta migración tiene algunas partes nuevas. Lo primero que notará es add :invoice_id, references(:invoices, type: :uuid, null: false)
. Esto crea el campo id_factura con una restricción en la base de datos que hace referencia a la tabla de facturas . Tenemos el mismo patrón para el campo item_id . Otra cosa que es diferente es la forma en que creamos un índice: create index(:invoice_items, [:invoice_id])
crea el índice bill_items_invoice_id_index .
Ecto.Schema y Ecto.Changeset
En Ecto, Ecto.Model
ha quedado obsoleto a favor del uso de Ecto.Schema
, por lo que llamaremos a los módulos esquemas en lugar de modelos. Vamos a crear los conjuntos de cambios. Comenzaremos con el elemento de conjunto de cambios más simple y crearemos el archivo 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 la parte superior, inyectamos código en el conjunto de cambios usando use Ecto.Schema
. También estamos utilizando import Ecto.Changeset
para importar la funcionalidad de Ecto.Changeset . Podríamos haber especificado qué métodos específicos importar, pero hagámoslo simple. El alias Cart.InvoiceItem
nos permite escribir directamente dentro del conjunto de cambios InvoiceItem , como verás en un momento.
Ecto.Esquema
@primary_key {:id, :binary_id, autogenerate: true}
especifica que nuestra clave principal se generará automáticamente. Como estamos usando un tipo de UUID, definimos el esquema con schema "items" do
y dentro del bloque definimos cada campo y relaciones. Definimos nombre como cadena y precio como decimal, muy similar a la migración. A continuación, la macro has_many :invoice_items, InvoiceItem
indica una relación entre Elemento y ElementoFactura . Dado que por convención nombramos el campo item_id en la tabla de elementos de factura , no necesitamos configurar la clave externa. Finalmente, el método de marcas de tiempo establecerá los campos insertado_en y actualizado_en .
Ecto.Conjunto de cambios
La función def changeset(data, params \\ %{}) do
recibe una estructura Elixir con parámetros que canalizaremos a través de diferentes funciones. cast(params, @fields)
los valores en el tipo correcto. Por ejemplo, puede pasar solo cadenas en los parámetros y se convertirán al tipo correcto definido en el esquema. validate_required([:name, :price])
valida que los campos nombre y precio estén presentes, validate_number(:price, greater_than_or_equal_to: Decimal.new(0))
valida que el número sea mayor o igual a 0 o en este caso Decimal.new(0)
.
Eso fue mucho para asimilar, así que veamos esto en la consola con ejemplos para que pueda comprender mejor los conceptos:
iex -S mix
Esto cargará la consola. -S mix
carga el proyecto actual en 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>
Esto devuelve una estructura Ecto.Changeset
que es válida sin errores. Ahora vamos a guardarlo:
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>}
No mostramos el SQL por brevedad. En este caso, devuelve la estructura Cart.Item con todos los valores establecidos. Puede ver que insert_at y updated_at contienen sus marcas de tiempo y el campo id tiene un valor UUID. Veamos otros casos:
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)
Ahora hemos configurado el artículo Scissors
de una manera diferente, configurando el precio directamente %Cart.Item{price: Decimal.new(20)}
. Necesitamos establecer su tipo correcto, a diferencia del primer elemento en el que acabamos de pasar una cadena como precio. Podríamos haber pasado un flotante y esto se habría convertido en un tipo decimal. Si aprobamos, por ejemplo, %Cart.Item{price: 12.5}
, cuando inserte el artículo, arrojará una excepción que indica que el tipo no coincide.

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>
Para terminar la consola, presione Ctrl+C dos veces. Puede ver que las validaciones están funcionando y el precio debe ser mayor o igual a cero (0). Como puede ver, hemos definido todo el esquema Ecto.Schema, que es la parte relacionada con cómo se define la estructura del módulo y el conjunto de cambios Ecto.Changeset, que es todas las validaciones y conversión. Continuemos y creemos el archivo 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
Este conjunto de cambios es más grande, pero ya debería estar familiarizado con la mayor parte. Aquí belongs_to :invoice, Cart.Invoice, type: :binary_id
define la relación "pertenece a" con el conjunto de cambios Carrito.Factura que pronto crearemos. El siguiente belongs_to :item
crea una relación con la tabla de elementos. Hemos definido @zero Decimal.new(0)
. En este caso, @zero es como una constante a la que se puede acceder dentro del módulo. La función changeset tiene partes nuevas, una de las cuales es foreign_key_constraint(:invoice_id, message: "Select a valid invoice")
. Esto permitirá que se genere un mensaje de error en lugar de generar una excepción cuando no se cumpla la restricción. Y finalmente, el método set_subtotal calculará el subtotal. Pasamos el conjunto de cambios y devolvemos un nuevo conjunto de cambios con el subtotal calculado si tenemos tanto el precio como la cantidad.
Ahora, creemos Cart.Invoice . Así que cree y edite el archivo lib/cart/invoice.ex
para que contenga lo siguiente:
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
El conjunto de cambios Cart.Invoice tiene algunas diferencias. El primero está dentro de schemas : has_many : invoice_items has_many :invoice_items, InvoiceItem, on_delete: :delete_all
significa que cuando eliminamos una factura, se eliminarán todos los elementos de factura asociados. Sin embargo, tenga en cuenta que esta no es una restricción definida en la base de datos.
Probemos el método de creación en la consola para entender mejor las cosas. Es posible que haya creado los elementos ("Papel", "Tijeras") que usaremos aquí:
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) }
Obtuvimos todos los artículos con Cart.Repo.all y con la función Enum.map solo obtenemos el item.id
de cada artículo. En la segunda línea, solo asignamos id1
e id2
con el primer y segundo item_id, respectivamente:
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 factura se ha creado con sus elementos_factura y ahora podemos obtener todas las facturas.
iex(4)> alias Cart.{Repo, Invoice} iex(5)> Repo.all(Invoice)
Puede ver que devuelve la factura , pero también nos gustaría ver los elementos de la factura :
iex(6)> Repo.all(Invoice) |> Repo.preload(:invoice_items)
Con la función Repo.preload , podemos obtener los elementos de invoice_items
. Tenga en cuenta que esto puede procesar consultas simultáneamente. En mi caso la consulta quedó así:
iex(7)> Repo.get(Invoice, "5d573153-b3d6-46bc-a2c0-6681102dd3ab") |> Repo.preload(:invoice_items)
Ecto.Consulta
Hasta ahora, hemos mostrado cómo crear nuevos artículos y nuevas facturas con relaciones. Pero ¿qué pasa con la consulta? Bueno, les presento Ecto.Query que nos ayudará a realizar consultas a la base de datos, pero primero necesitamos más datos para explicarnos mejor.
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")})
Ahora deberíamos tener 8 elementos y hay un "Chocolate" repetido. Es posible que queramos saber qué elementos se repiten. Así que probemos esta consulta:
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"}]
Puede ver que en la consulta queríamos devolver un mapa con el nombre del elemento y la cantidad de veces que aparece en la tabla de elementos. Alternativamente, sin embargo, es más probable que nos interese ver cuáles son los productos más vendidos. Entonces, para eso, creemos algunas facturas. Primero, hagamos nuestras vidas más fáciles creando un mapa para acceder a 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"}
Como puede ver, hemos creado un mapa usando una comprensión
iex(12)> line_items = [%{item_id: items["Chocolates"], quantity: 2}]
Necesitamos agregar el precio en los invoice_items
de elementos de factura para crear una factura, pero sería mejor simplemente pasar la identificación del artículo y hacer que el precio se llene automáticamente. Realizaremos cambios en el módulo Cart.Invoice para lograr esto:
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
Lo primero que notará es que hemos agregado Ecto.Query , que nos permitirá consultar la base de datos. La nueva función es defp items_with_prices(items) do
que busca a través de los artículos y encuentra y establece el precio de cada artículo.
Primero, defp items_with_prices(items) do
recibe una lista como argumento. Con item_ids = Enum.map(items, fn(item) -> item[:item_id] || item["item_id"] end)
, iteramos a través de todos los elementos y obtenemos solo el item_id . Como puedes ver, accedemos con atom :item_id
o string “item_id”, ya que los mapas pueden tener cualquiera de estos como claves. La consulta q = from(i in Item, select: %{id: i.id, price: i.price}, where: i.id in ^item_ids)
encontrará todos los elementos que están en item_ids
y devolverá un mapa con item.id
y item.price
. Luego podemos ejecutar la consulta prices = Repo.all(q)
que devuelve una lista de mapas. Luego necesitamos iterar a través de los elementos y crear una nueva lista que agregará el precio. Enum.map(items, fn(item) ->
itera a través de cada elemento, encuentra el precio Enum.find(prices, fn(p) -> p[:id] == item_id end)[:price] || 0
, y crea una nueva lista con item_id
, cantidad y precio, y con eso, ya no es necesario agregar el precio en cada uno de los elementos de invoice_items
.
Insertar más facturas
Como recordará, anteriormente creamos un mapa de elementos que nos permite acceder a la identificación usando el nombre del elemento para, por ejemplo items["Gum"]
"cb1c5a93-ecbf-4e4b-8588-cc40f7d12364". Esto simplifica la creación de elementos de factura . Vamos a crear más facturas. Inicie la consola nuevamente y ejecute:
Iex -S mix
iex(1)> Repo.delete_all(InvoiceItem); Repo.delete_all(Invoice)
Eliminamos todos los elementos de factura y las facturas para tener una pizarra en blanco:
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})
Ahora tenemos 3 facturas; el primero con 2 artículos, el segundo con 3 artículos y el tercero con 6 artículos. Ahora nos gustaría saber qué productos son los artículos más vendidos. Para responder a eso, vamos a crear una consulta para encontrar los artículos más vendidos por cantidad y por subtotal (precio x cantidad).
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
Importamos Ecto.Query y luego creamos un alias Cart.{InvoiceItem, Item, Repo}
por lo que no necesitamos agregar Cart al comienzo de cada módulo. La primera función items_by_quantity llama a la función items_by
, pasando el parámetro :quantity
y llamando a Repo.all para ejecutar la consulta. La función items_by_subtotal es similar a la función anterior pero pasa el parámetro :subtotal
. Ahora expliquemos items_by :
-
from i in Item
, esta macro selecciona el módulo Item -
join: ii in InvoiceItem, on: ii.item_id == i.id
, crea una combinación en la condición “items.id = factura_items.item_id” -
select: %{id: i.id, name: i.name, total: sum(field(ii, ^type))}
, estamos generando un mapa con todos los campos que queremos primero seleccionamos el id y el nombre de Item y hacemos una suma de operadores. El campo (ii, ^ tipo) usa el campo macro para acceder dinámicamente a un campo -
group_by: i.id
, agrupamos por items.id -
order_by: [desc: sum(field(ii, ^type))]
y finalmente ordenar por la suma en orden descendente
Hasta ahora hemos escrito la consulta en estilo de lista, pero podríamos reescribirla en estilo 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
Prefiero escribir consultas en forma de lista ya que me resulta más legible.
Conclusión
Hemos cubierto una buena parte de lo que puedes hacer en una aplicación con Ecto. Por supuesto, hay mucho más que puedes aprender de los documentos de Ecto. Con Ecto, puede crear aplicaciones concurrentes tolerantes a fallas con poco esfuerzo que pueden escalar fácilmente gracias a la máquina virtual Erlang. Ecto proporciona la base para el almacenamiento en sus aplicaciones Elixir y proporciona funciones y macros para administrar fácilmente sus datos.
En este tutorial, examinamos Ecto.Schema, Ecto.Changeset, Ecto.Migration, Ecto.Query y Ecto.Repo. Cada uno de estos módulos lo ayuda en diferentes partes de su aplicación y hace que el código sea más explícito y más fácil de mantener y comprender.
Si desea consultar el código del tutorial, puede encontrarlo aquí en GitHub.
Si le gustó este tutorial y está interesado en obtener más información, recomendaría Phoenix (para obtener una lista de proyectos increíbles), Awesome Elixir y esta charla que compara ActiveRecord con Ecto.