Ecto é uma linguagem específica de domínio para escrever consultas e interagir com bancos de dados no Linguagem elixir . A versão mais recente (2.0) oferece suporte a PostgreSQL e MySQL. (o suporte para MSSQL, SQLite e MongoDB estará disponível no futuro). Caso você seja novo em Elixir ou tenha pouca experiência com isso, eu recomendo que você leia o de Kleber Virgilio Correia Introdução à linguagem de programação Elixir .
Ecto é composto por quatro componentes principais:
Para este tutorial, você precisará de:
Para começar, vamos criar um novo aplicativo com um supervisor usando o Mix. Misturar é uma ferramenta de construção fornecida com 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 do projeto inicial:
* 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 o --sup
uma vez que precisamos de uma árvore de supervisor que manterá a conexão com o banco de dados. Em seguida, vamos para o cart
diretório com cd cart
e abra o arquivo mix.exs
e substitua 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
Em def application do
temos que adicionar como aplicativos :postgrex, :ecto
para que possam ser usados dentro de nosso aplicativo. Também precisamos adicioná-los como dependências adicionando 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 a Gemfile.lock
no bundler).
Agora veremos como definir um repo em nosso aplicativo. Podemos ter mais de um repo, 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 repo. Se você ler a saída, ela lhe dirá para adicionar um supervisor e um repo 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 adicioná-lo à lista de filhos (no Elixir, as listas são semelhantes a matrizes). Definimos as crianças supervisionadas com a estratégia strategy: :one_for_one
o que significa que, se um dos processos supervisionados falhar, o supervisor irá reiniciar apenas aquele processo em seu estado padrão. Você pode aprender mais sobre supervisores Aqui . Se você olhar para lib/cart/repo.ex
você verá que este arquivo já foi criado, o que significa que temos um Repo para nosso aplicativo.
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 todas as configurações para nosso banco de dados, podemos agora 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 vamos definir o escopo de nosso aplicativo primeiro.
Para nosso aplicativo de demonstração, construiremos uma ferramenta de faturamento simples. Para changesets (modelos), teremos Fatura , Item e InvoiceItem . InvoiceItem pertence a Fatura e Item . Este diagrama representa como nossos modelos estarão relacionados entre si:
O diagrama é muito simples. Temos uma mesa faturas que tem muitos invoice_items onde guardamos todos os detalhes e também uma mesa Itens que tem muitos invoice_items . Você pode ver que o tipo de invoice_id e item_id dentro invoice_items tabela é UUID. Estamos usando UUID porque ajuda a ofuscar as rotas, caso você queira expor o aplicativo por meio de uma API e torna mais simples a sincronização, já que você não depende de um número sequencial. Agora vamos criar as tabelas usando tarefas Mix.
Migrações são arquivos usados para modificar o esquema do banco de dados. 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 irá 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 que fique da seguinte maneira:
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
Método interno def change do
definimos o esquema que irá gerar o SQL para o banco de dados. create table(:invoices, primary_key: false) do
irá criar a mesa faturas . Definimos primary_key: false
mas vamos adicionar um campo de ID do tipo UUID , campo de cliente do tipo texto, campo de data do tipo data. O timestamps
método 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 o Itens mesa:
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, 2 dos quais são para a parte decimal do número. Vamos executar a migração novamente:
mix ecto.migrate
Agora nós criamos Itens mesa e, finalmente, vamos criar o invoice_items mesa:
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 ao faturas mesa. Temos o mesmo padrão para item_id campo. Outra coisa diferente é a forma como criamos um índice: create index(:invoice_items, [:invoice_id])
cria o índice invoice_items_invoice_id_index .
Em Ecto, Ecto.Model
tornou-se obsoleto em favor do uso de Ecto.Schema
, portanto, chamaremos os esquemas de módulos em vez de modelos. Vamos criar os changesets. Começaremos com o item do changeset mais simples e criaremos 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
Na parte superior, injetamos código no conjunto de alterações usando use Ecto.Schema
. Também estamos usando import Ecto.Changeset
para importar funcionalidade de Ecto.Changeset . Poderíamos ter especificado quais métodos específicos importar, mas vamos mantê-lo simples. O alias Cart.InvoiceItem
nos permite escrever diretamente dentro do changeset InvoiceItem , como você verá em um momento.
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 schema 'items' do
e dentro do bloco definimos cada campo e relacionamento. Nós definimos nome como corda 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 . Já que por convenção, chamamos o campo item_id no invoice_items tabela, não precisamos configurar a chave estrangeira. Finalmente, o timestamps método irá definir o inserido_em e updated_at Campos.
O def changeset(data, params \ %{}) do
função recebe uma estrutura Elixir com parâmetros que iremos tubo através de diferentes funções. cast(params, @fields)
converte os valores no tipo correto. Por exemplo, você pode passar apenas strings nos parâmetros e eles seriam convertidos para o tipo correto definido no esquema. validate_required([:name, :price])
valida que o nome e preço campos 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)
.
Foi muito para assimilar, então vamos dar uma olhada no console com exemplos para que você possa entender melhor os conceitos:
iex -S mix
Isso irá carregar o console. -S mix
carrega o projeto atual no REPL iex.
iex(0)> item = Cart.Item.changeset(%Cart.Item{}, %{name: 'Paper', price: '2.5'}) #Ecto.Changeset
Isso retorna um Ecto.Changeset
estrutura que é válida sem erros. Agora vamos salvá-lo:
iex(1)> item = Cart.Repo.insert!(item) %Cart.Item{__meta__: #Ecto.Schema.Metadata, id: '66ab2ab7-966d-4b11-b359-019a422328d7', inserted_at: #Ecto.DateTime, invoice_items: #Ecto.Association.NotLoaded, name: 'Paper', price: #Decimal, updated_at: #Ecto.DateTime}
Não mostramos o SQL por questões de brevidade. Neste caso, ele retorna o Cart.Item estrutura com todos os valores definidos, você pode ver que inserido_em e updated_at conter seus carimbos de data / hora e o Eu iria campo tem um valor UUID. Vejamos alguns outros casos:
iex(3)> item2 = Cart.Item.changeset(%Cart.Item{price: Decimal.new(20)}, %{name: 'Scissors'}) #Ecto.Changeset iex(4)> Cart.Repo.insert(item2)
Agora definimos o Scissors
item de uma maneira diferente, definindo o preço diretamente %Cart.Item{price: Decimal.new(20)}
. Precisamos definir seu tipo correto, ao contrário do primeiro item, onde passamos uma string como preço. Poderíamos ter passado um float e isso seria 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
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, nós 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 elenco. 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.data.price), (cs.changes[:quantity] do {_price, nil} -> cs {nil, _quantity} -> cs {price, quantity} -> put_change(cs, :subtotal, Decimal.mult(price, quantity)) end end end
Este conjunto de alterações é maior, mas você já deve estar familiarizado com a maior parte dele. Aqui belongs_to :invoice, Cart.Invoice, type: :binary_id
define a relação 'pertence a' com o Cart.Invoice conjunto de alterações que criaremos em breve. O próximo belongs_to :item
cria um relacionamento com a tabela de itens. Definimos @zero Decimal.new(0)
. Nesse 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 irá 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
Cart.Invoice conjunto de mudanças tem algumas diferenças. O primeiro está dentro esquemas : has_many :invoice_items, InvoiceItem, on_delete: :delete_all
significa que quando excluímos uma fatura, todos os associados invoice_items será deletado. Lembre-se, porém, de que essa não é uma restrição definida no banco de dados.
Vamos tentar o método de criação no console para entender melhor as coisas. Você pode ter criado os itens (“Papel”, “Tesouras”) 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 o Enum.map função acabamos de obter a item.id
de cada item. Na segunda linha, apenas atribuímos id1
e id2
com o primeiro e o 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 obter todas as faturas agora.
iex(4)> alias Cart.{Repo, Invoice} iex(5)> Repo.all(Invoice)
Você pode ver que ele retorna o Fatura mas gostaríamos de ver também o invoice_items :
iex(6)> Repo.all(Invoice) |> Repo.preload(:invoice_items)
Com o Repo.preload função, podemos obter o invoice_items
. Observe que isso pode processar consultas simultaneamente. No meu caso, a consulta parecia assim:
iex(7)> Repo.get(Invoice, '5d573153-b3d6-46bc-a2c0-6681102dd3ab') |> Repo.preload(:invoice_items)
Até agora, mostramos como criar novos itens e novas faturas com relacionamentos. Mas e quanto a consultar? Bem, deixe-me apresentar-lhe Ecto.Query o 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 provavelmente estar interessados em ver quais são os produtos mais vendidos. Então, para isso, vamos criar algumas faturas. Primeiro, vamos tornar nossas vidas mais fáceis 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} '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 um compreensão
iex(12)> line_items = [%{item_id: items['Chocolates'], quantity: 2}]
Precisamos adicionar o preço no invoice_items
params para criar uma nota fiscal, mas seria melhor apenas passar o id do item e ter o preço preenchido automaticamente. Faremos alterações no Cart.Invoice módulo 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'] % end) end
A primeira coisa que você notará é que adicionamos Ecto.Query , o que nos permitirá consultar o banco de dados. A nova função é defp items_with_prices(items) do
que pesquisa os itens, localiza 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 qualquer um com atom :item_id
ou string “item_id”, uma vez 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 iterar os itens e criar uma nova lista que adicionará o preço. O Enum.map(items, fn(item) ->
itera em 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
.
Como você se lembra, anteriormente criamos um mapa Itens que nos permite acessar o Eu iria usando o nome do item para, por exemplo, items['Gum']
“Cb1c5a93-ecbf-4e4b-8588-cc40f7d12364”. Isso torna mais simples criar 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)
Nós apagamos tudo invoice_items e as faturas devem ter 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 notas fiscais; o primeiro com 2 itens, o segundo com 3 itens e o terceiro com 6 itens. Gostaríamos agora de saber quais são os produtos 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
Nós importamos Ecto.Query e então alias Cart.{InvoiceItem, Item, Repo}
portanto, não precisamos adicionar o carrinho no início de cada módulo. A primeira função items_by_quantity chama o items_by
, passando a função :quantity
parâmetro e chamando o Repo.all para executar a consulta. A função items_by_subtotal é semelhante à função anterior, mas passa o :subtotal
parâmetro. Agora vamos explicar items_by :
from i in Item
, esta macro seleciona o módulo Itemjoin: 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 campogroup_by: i.id
, Agrupamos por items.idorder_by: [desc: sum(field(ii, ^type))]
e, finalmente, ordenar pela soma em ordem decrescenteAté 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
Eu prefiro escrever consultas em forma de lista, pois acho mais legível.
Cobrimos uma boa parte do que você pode fazer em um aplicativo com o Ecto. Claro, há muito mais que você pode aprender com o Ecto docs . Com o Ecto, você pode criar aplicativos simultâneos e tolerantes a falhas com pouco esforço que podem ser escalonados facilmente graças à máquina virtual Erlang. 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 verificar o código do tutorial, pode encontrá-lo Aqui no GitHub.
Se você gostou deste tutorial e está interessado em mais informações, eu recomendo Fénix (para uma lista de projetos incríveis), Elixir incrível e esta conversa que compara ActiveRecord com Ecto.