Conheça o Ecto, um Database Wrapper sem compromisso para aplicativos Elixir simultâneos

Publicados: 2022-03-11

Ecto é uma linguagem específica de domínio para escrever consultas e interagir com bancos de dados na linguagem Elixir. A versão mais recente (2.0) suporta PostgreSQL e MySQL. (suporte para MSSQL, SQLite e MongoDB estará disponível no futuro). Caso você seja novo no Elixir ou tenha pouca experiência com ele, eu recomendo que você leia Getting Started with Elixir Programming Language de Kleber Virgilio Correia.

Cansado de todos os dialetos SQL? Fale com seu banco de dados através da Ecto.
Tweet

O Ecto é composto por quatro componentes principais:

  • Ecto.Repo. Define repositórios que são wrappers em torno de um armazenamento de dados. Usando-o, podemos inserir, criar, excluir e consultar um repositório. Um adaptador e credenciais são necessários para se comunicar com o banco de dados.
  • Ecto.Schema. Os esquemas são usados ​​para mapear qualquer fonte de dados em uma estrutura Elixir.
  • Ecto.Changeset. Os conjuntos de alterações fornecem uma maneira para os desenvolvedores filtrarem e converterem parâmetros externos, bem como um mecanismo para rastrear e validar as alterações antes de serem aplicadas aos dados.
  • Ecto.Consulta. Fornece uma consulta SQL semelhante a DSL para recuperar informações de um repositório. As consultas no Ecto são seguras, evitando problemas comuns como SQL Injection, enquanto ainda podem ser compostas, permitindo que os desenvolvedores construam consultas peça por peça em vez de todas de uma vez.

Para este tutorial, você precisará de:

  • Elixir instalado (guia de instalação para 1.2 ou posterior)
  • PostgreSQL instalado
  • Um usuário definido com permissão para criar um banco de dados (Nota: Usaremos o usuário “postgres” com a senha “postgres” como exemplo ao longo deste tutorial.)

Instalação e configuração

Para começar, vamos criar um novo aplicativo com um supervisor usando o Mix. Mix é uma ferramenta de compilação que acompanha o Elixir que fornece tarefas para criar, compilar, testar seu aplicativo, gerenciar suas dependências e muito mais.

 mix new cart --sup

Isso criará um carrinho de diretório com os arquivos iniciais do projeto:

 * 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 a opção --sup pois precisamos de uma árvore supervisora ​​que manterá a conexão com o banco de dados. Em seguida, vamos ao diretório cart com cd cart e abrimos o arquivo mix.exs e substituímos seu conteúdo:

 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

Na def application do temos que adicionar como aplicações :postgrex, :ecto para que possam ser usadas dentro da nossa aplicação. Também temos que adicioná-los como dependências adicionando em defp deps do postgrex (que é o adaptador de banco de dados) e ecto . Depois de editar o arquivo, execute no console:

 mix deps.get

Isso instalará todas as dependências e criará um arquivo mix.lock que armazena todas as dependências e subdependências dos pacotes instalados (semelhante ao Gemfile.lock no bundler).

Ecto.Repo

Veremos agora como definir um repositório em nosso aplicativo. Podemos ter mais de um repositório, o que significa que podemos nos conectar a mais de um banco de dados. Precisamos configurar o banco de dados no arquivo config/config.exs :

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

Estamos apenas definindo o mínimo, para que possamos executar o próximo comando. Com a linha :cart, cart_repos: [Cart.Repo] estamos dizendo ao Ecto quais repositórios estamos usando. Este é um recurso interessante, pois nos permite ter muitos repositórios, ou seja, podemos nos conectar a vários bancos de dados.

Agora execute o seguinte 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 gera o repositório. Se você ler a saída, ela informa para adicionar um supervisor e um repositório em seu aplicativo. Vamos começar com o supervisor. Vamos editar 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

Neste arquivo, estamos definindo o supervisor supervisor(Cart.Repo, []) e adicionando-o à lista de filhos (no Elixir, as listas são semelhantes aos arrays). Definimos os filhos supervisionados com a estratégia de strategy: :one_for_one o que significa que, se um dos processos supervisionados falhar, o supervisor reiniciará apenas esse processo em seu estado padrão. Você pode saber mais sobre supervisores aqui. Se você olhar em lib/cart/repo.ex verá que este arquivo já foi criado, o que significa que temos um Repo para nossa aplicação.

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

Agora vamos editar o arquivo de configuração 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"

Tendo definido toda a configuração para nosso banco de dados, agora podemos gerá-lo executando:

 mix ecto.create

Este comando cria o banco de dados e, com isso, basicamente finalizamos a configuração. Agora estamos prontos para começar a codificar, mas primeiro vamos definir o escopo do nosso aplicativo.

Criando uma fatura com itens embutidos

Para nosso aplicativo de demonstração, criaremos uma ferramenta de faturamento simples. Para changesets (modelos) teremos Invoice , Item e InvoiceItem . InvoiceItem pertence a Invoice e Item . Este diagrama representa como nossos modelos serão relacionados entre si:

O diagrama é bem simples. Temos uma tabela invoices que possui muitos invoice_items onde armazenamos todos os detalhes e também uma tabela items que possui muitos invoice_items . Você pode ver que o tipo para invoice_id e item_id na tabela invoice_items é UUID. Estamos usando UUID porque ajuda a ofuscar as rotas, caso você queira expor o aplicativo por uma API e simplifica a sincronização, pois você não depende de um número sequencial. Agora vamos criar as tabelas usando as tarefas Mix.

Ecto.Migração

Migrações são arquivos usados ​​para modificar o esquema do banco de dados. O Ecto.Migration oferece um conjunto de métodos para criar tabelas, adicionar índices, criar restrições e outras coisas relacionadas ao esquema. As migrações realmente ajudam a manter o aplicativo sincronizado com o banco de dados. Vamos criar um script de migração para nossa primeira tabela:

 mix ecto.gen.migration create_invoices

Isso gerará um arquivo semelhante a priv/repo/migrations/20160614115844_create_invoices.exs onde definiremos nossa migração. Abra o arquivo gerado e modifique seu conteúdo para ficar da seguinte forma:

 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 o esquema que irá gerar o SQL para o banco de dados. create table(:invoices, primary_key: false) do criará a tabela invoices . Definimos primary_key: false mas adicionaremos um campo de ID do tipo UUID, campo de cliente do tipo texto, campo de data do tipo data. O método timestamps irá gerar os campos inserted_at e updated_at que o Ecto preenche automaticamente com a hora em que o registro foi inserido e a hora em que foi atualizado, respectivamente. Agora vá para o console e execute a migração:

 mix ecto.migrate

Criamos a tabela invoice s com todos os campos definidos. Vamos criar a tabela de itens :

 mix ecto.gen.migration create_items

Agora edite o script de migração gerado:

 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

A novidade aqui é o campo decimal que permite números com 12 dígitos, sendo 2 deles para a parte decimal do número. Vamos executar a migração novamente:

 mix ecto.migrate

Agora criamos a tabela de itens e finalmente vamos criar a tabela invoice_items :

 mix ecto.gen.migration create_invoice_items

Edite a migração:

 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 você pode ver, essa migração tem algumas partes novas. A primeira coisa que você notará é add :invoice_id, references(:invoices, type: :uuid, null: false) . Isso cria o campo invoice_id com uma restrição no banco de dados que faz referência à tabela de faturas . Temos o mesmo padrão para o campo item_id . Outra coisa que é diferente é a forma como criamos um índice: create index(:invoice_items, [:invoice_id]) cria o índice invoice_items_invoice_id_index .

Ecto.Schema e Ecto.Changeset

No Ecto, Ecto.Model foi preterido em favor do uso de Ecto.Schema , então chamaremos os módulos de esquemas em vez de modelos. Vamos criar os changesets. Vamos começar com o mais simples changeset Item e criar o arquivo 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

No topo, injetamos código no changeset usando o use Ecto.Schema . Também estamos usando import Ecto.Changeset para importar a funcionalidade de Ecto.Changeset . Poderíamos ter especificado quais métodos específicos importar, mas vamos simplificar. O alias Cart.InvoiceItem nos permite escrever diretamente dentro do changeset InvoiceItem , como você verá em breve.

Ecto.Esquema

O @primary_key {:id, :binary_id, autogenerate: true} especifica que nossa chave primária será gerada automaticamente. Como estamos usando um tipo UUID, definimos o esquema com os schema "items" do e dentro do bloco definimos cada campo e relacionamentos. Definimos nome como string e preço como decimal, muito semelhante à migração. Em seguida, a macro has_many :invoice_items, InvoiceItem indica uma relação entre Item e InvoiceItem . Como por convenção nomeamos o campo item_id na tabela invoice_items , não precisamos configurar a chave estrangeira. Finalmente, o método timestamps definirá os campos insert_at e updated_at .

Ecto.Changeset

A função def changeset(data, params \\ %{}) do recebe uma estrutura Elixir com params que iremos canalizar através de diferentes funções. cast(params, @fields) os valores no tipo correto. Por exemplo, você pode passar apenas strings nos parâmetros e esses serão convertidos para o tipo correto definido no esquema. validate_required([:name, :price]) valida se os campos name e price estão presentes, validate_number(:price, greater_than_or_equal_to: Decimal.new(0)) valida se o número é maior ou igual a 0 ou neste caso Decimal.new(0) .

No Elixir, as operações decimais são feitas de maneira diferente, pois são implementadas como uma estrutura.

Isso foi muito para assimilar, então vamos ver isso no console com exemplos para que você possa entender melhor os conceitos:

 iex -S mix

Isso carregará o console. -S mix carrega o projeto atual no 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>

Isso retorna uma estrutura Ecto.Changeset que é válida sem erros. Agora vamos salvá-lo:

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

Não mostramos o SQL por brevidade. Neste caso, ele retorna a estrutura Cart.Item com todos os valores definidos, você pode ver que insert_at e updated_at contêm seus timestamps e o campo id tem um valor UUID. Vejamos alguns outros 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)

Agora definimos o item Scissors de uma maneira diferente, definindo o preço diretamente %Cart.Item{price: Decimal.new(20)} . Precisamos definir seu tipo correto, diferente do primeiro item em que acabamos de passar uma string como preço. Poderíamos ter passado um float e isso teria sido convertido em um tipo decimal. Se passarmos, por exemplo %Cart.Item{price: 12.5} , quando você inserir o item ele lançará uma exceção informando que o tipo não corresponde.

 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 encerrar o console, pressione Ctrl+C duas vezes. Você pode ver que as validações estão funcionando e o preço deve ser maior ou igual a zero (0). Como você pode ver, definimos todo o esquema Ecto.Schema que é a parte relacionada a como a estrutura do módulo é definida e o changeset Ecto.Changeset que é todas as validações e casting. Vamos continuar e criar o arquivo 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 changeset é maior, mas você já deve estar familiarizado com a maior parte dele. Aqui belongs_to :invoice, Cart.Invoice, type: :binary_id define o relacionamento “pertence a” com o changeset Cart.Invoice que iremos criar em breve. O próximo belongs_to :item cria um relacionamento com a tabela de itens. Definimos @zero Decimal.new(0) . Neste caso, @zero é como uma constante que pode ser acessada dentro do módulo. A função changeset tem novas partes, uma das quais é foreign_key_constraint(:invoice_id, message: "Select a valid invoice") . Isso permitirá que uma mensagem de erro seja gerada em vez de gerar uma exceção quando a restrição não for cumprida. E finalmente, o método set_subtotal calculará o subtotal. Passamos o changeset e retornamos um novo changeset com o subtotal calculado se tivermos o preço e a quantidade.

Agora, vamos criar o Cart.Invoice . Portanto, crie e edite o arquivo lib/cart/invoice.ex para conter o seguinte:

 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

O changeset Cart.Invoice tem algumas diferenças. O primeiro está dentro dos esquemas : has_many :invoice_items, InvoiceItem, on_delete: :delete_all significa que quando excluímos uma fatura, todos os invoice_items associados serão excluídos. Tenha em mente, porém, que esta não é uma restrição definida no banco de dados.

Vamos tentar o método create no console para entender melhor as coisas. Você pode ter criado os itens (“Papel”, “Tesoura”) que usaremos aqui:

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

Buscamos todos os itens com Cart.Repo.all e com a função Enum.map apenas obtemos o item.id de cada item. Na segunda linha, apenas atribuímos id1 e id2 com o primeiro e segundo item_ids, 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})

A fatura foi criada com seus invoice_items e podemos buscar todas as faturas agora.

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

Você pode ver que ele retorna a fatura , mas gostaríamos de ver também o invoice_items :

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

Com a função Repo.preload , podemos obter o invoice_items . Observe que isso pode processar consultas simultaneamente. No meu caso a consulta ficou assim:

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

Ecto.Consulta

Até agora, mostramos como criar novos itens e novas faturas com relacionamentos. Mas e a consulta? Bem, deixe-me apresentar-lhe o Ecto.Query que nos ajudará a fazer consultas ao banco de dados, mas primeiro precisamos de mais dados para explicar melhor.

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

Devemos agora ter 8 itens e há um “Chocolate” repetido. Podemos querer saber quais itens são repetidos. Então vamos tentar 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"}]

Você pode ver que na consulta queríamos retornar um mapa com o nome do item e o número de vezes que ele aparece na tabela de itens. Alternativamente, porém, podemos estar mais interessados ​​em ver quais são os produtos mais vendidos. Então, para isso, vamos criar algumas faturas. Primeiro, vamos facilitar nossas vidas criando um mapa para acessar um 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 você pode ver, criamos um mapa usando uma compreensão

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

Precisamos adicionar o preço nos parâmetros invoice_items para criar uma fatura, mas seria melhor apenas passar o id do item e ter o preço preenchido automaticamente. Faremos alterações no módulo Cart.Invoice para fazer isso:

 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

A primeira coisa que você notará é que adicionamos Ecto.Query , que nos permitirá consultar o banco de dados. A nova função é defp items_with_prices(items) do que pesquisa os itens e encontra e define o preço de cada item.

Primeiro, defp items_with_prices(items) do recebe uma lista como argumento. Com item_ids = Enum.map(items, fn(item) -> item[:item_id] || item["item_id"] end) , iteramos por todos os itens e obtemos apenas o item_id . Como você pode ver, acessamos com atom :item_id ou string “item_id”, já que os mapas podem ter qualquer um deles como chaves. A consulta q = from(i in Item, select: %{id: i.id, price: i.price}, where: i.id in ^item_ids) encontrará todos os itens que estão em item_ids e retornará um mapa com item.id e item.price . Podemos então executar a consulta prices = Repo.all(q) que retorna uma lista de mapas. Em seguida, precisamos percorrer os itens e criar uma nova lista que adicionará o preço. O Enum.map(items, fn(item) -> itera através de cada item, encontra o preço Enum.find(prices, fn(p) -> p[:id] == item_id end)[:price] || 0 , e cria uma nova lista com item_id , quantidade e preço, e com isso não é mais necessário adicionar o preço em cada um dos invoice_items .

Inserindo mais faturas

Como você lembra, anteriormente criamos um mapa de itens que nos permite acessar o id usando o nome do item para ie items["Gum"] “cb1c5a93-ecbf-4e4b-8588-cc40f7d12364”. Isso simplifica a criação de invoice_items . Vamos criar mais faturas. Inicie o console novamente e execute:

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

Excluímos todos os invoice_items e faturas para ficar com uma folha em branco:

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

Agora temos 3 faturas; a primeira com 2 itens, a segunda com 3 itens e a terceira com 6 itens. Agora gostaríamos de saber quais produtos são os itens mais vendidos? Para responder a isso, vamos criar uma consulta para encontrar os itens mais vendidos por quantidade e por subtotal (preço x quantidade).

 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 e então apelidamos alias Cart.{InvoiceItem, Item, Repo} para não precisarmos adicionar Cart no início de cada módulo. A primeira função items_by_quantity chama a função items_by , passando o parâmetro :quantity e chamando o Repo.all para executar a consulta. A função items_by_subtotal é semelhante à função anterior, mas passa o parâmetro :subtotal . Agora vamos explicar items_by :

  • from i in Item , esta macro seleciona o módulo Item
  • join: ii in InvoiceItem, on: ii.item_id == i.id , cria uma junção na condição “items.id = invoice_items.item_id”
  • select: %{id: i.id, name: i.name, total: sum(field(ii, ^type))} , estamos gerando um mapa com todos os campos que queremos primeiro selecionamos o id e o nome do Item e fazemos uma soma de operadores. O campo(ii, ^type) usa o campo macro para acessar dinamicamente um campo
  • group_by: i.id , Agrupamos por items.id
  • order_by: [desc: sum(field(ii, ^type))] e finalmente ordenar pela soma em ordem decrescente

Até agora escrevemos a consulta no estilo de lista, mas podemos reescrevê-la no 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

Prefiro escrever consultas em forma de lista, pois acho mais legível.

Conclusão

Cobrimos uma boa parte do que você pode fazer em um aplicativo com Ecto. Claro, há muito mais que você pode aprender com os documentos do Ecto. Com o Ecto, você pode criar aplicativos simultâneos e tolerantes a falhas com pouco esforço que podem ser dimensionados facilmente graças à máquina virtual Erlang. O Ecto fornece a base para o armazenamento em seus aplicativos Elixir e fornece funções e macros para gerenciar facilmente seus dados.

Neste tutorial, examinamos Ecto.Schema, Ecto.Changeset, Ecto.Migration, Ecto.Query e Ecto.Repo. Cada um desses módulos ajuda você em diferentes partes do seu aplicativo e torna o código mais explícito e fácil de manter e entender.

Se você quiser conferir o código do tutorial, você pode encontrá-lo aqui no GitHub.

Se você gostou deste tutorial e está interessado em mais informações, eu recomendaria Phoenix (para uma lista de projetos incríveis), Awesome Elixir e esta palestra que compara ActiveRecord com Ecto.