Capa de Persistencia
con Ecto
Rafael Gutiérrez, @abaddon_gtz
Julio 23, 2016
@ElixirLangMx
Ecto
● Framework de persistencia para Elixir (hecho en Elixir)
● Diseñado para bases de datos relacionales
● Ideas de LINQ de .NET y ActiveRecord de Rails
● Wrapper de la base de datos
● Lenguaje de consulta integrado (language integrated
query - LIQ) !!
○ DSL
LINQ & Ecto & SQL
# Elixir
from c in Customer,
where: c.city == “Mexico”,
select: c.company_name
// C#
from c in db.Customers
where c.City == “Mexico”
select c.CompanyName
-- SQL
SELECTcompany_name
FROM customer
WHERE city = ‘Mexico’
Beneficios de un LIQ
● Metadatos
● Validación de sintaxis en tiempo de compilación
● Tipos
● Seguridad
● Composición
Componentes de Ecto
● Ecto.Repo
● Ecto.Schema
● Ecto.Changeset
● Ecto.Query
Caso de Estudio
Administrar las propuestas de Workshops
Ecto.Repo
● Wrapper de la base de datos
● Via el Repo, creamos, consultamos, borramos y
actualizamos los datos
● Usa un Adapter para comunicarse a la BD
Crear el proyecto con mix
● > mix new wsp_app --sup
● --sup para generar una aplicación OTP con árbol de
supervisión
● Ecto.Repo corre bajo un árbol de supervisión
Dependencias
# mix.exs
defp deps do
[{:postgrex, ">= 0.0.0"},
{:ecto, "~> 2.0.0"}]
end
# mix.exs
def application do
[applications: [:logger, :ecto, :postgrex],
mod: {WspApp, []}]
end
Repositorio y árbol de supervisión
# Nuevo archivo /lib/wsp_app/repo.ex
defmodule WspApp.Repo do
use Ecto.Repo, otp_app: :wsp_app
End
# Agregar el repo al árbol de supervisión /lib/wsp_app.ex
def start(_type, _args) do
import Supervisor.Spec, warn: false
children = [supervisor(WspApp.Repo, [])]
opts = [strategy: :one_for_one, name: WspApp.Supervisor]
Supervisor.start_link(children, opts)
end
Adapter
# Configurar el Adapter /config/config.exs
config :wsp_app, WspApp.Repo,
adapter: Ecto.Adapters.Postgres,
database: "ws_proposal_app_dev",
username: "postgres",
password: "",
hostname: "localhost"
# pool_size: 20
# desde Ecto 2
config :wsp_app, ecto_repos: [WspApp.Repo]
Ecto.Schema
● Mapea la fuente de datos a un struct de Elixir
● Usado para mapear la tabla a datos de Elixir
Migration - workshop_proposals
# > mix ecto.gen.migration create_workshop_proposals_table
# /priv/repo/migrations/
defmodule WspApp.Repo.Migrations.CreateWorkshopProposalsTable do
use Ecto.Migration
def change do
create table(:workshop_proposals) do
add :title, :text
add :description, :text
add :instructor_email, :text
add :tentative_date, :date
timestamps
end
end
end
Migration - votes
# > mix ecto.gen.migration create_votes_table
# /priv/repo/migrations/
defmodule WspApp.Repo.Migrations.CreateVotesTable do
use Ecto.Migration
def change do
create table(:votes) do
add :email, :text
add :workshop_proposal_id,
references(:workshop_proposals, [on_delete: :delete_all, on_update: :update_all])
timestamps
end
end
end
Migration - unique index para votes
# > mix ecto.gen.migration create_unique_vote_constraint
# /priv/repo/migrations/
defmodule WspApp.Repo.Migrations.CreateUniqueVoteConstraint do
use Ecto.Migration
def change do
create unique_index(:votes, [:email, :workshop_proposal_id])
end
end
Modelo - WorkshopProposal
# /lib/wsp_app/workshop_proposal.ex
defmodule WspApp.WorkshopProposal do
use Ecto.Schema
schema "workshop_proposals" do
field :title, :string
field :description, :string
field :instructor_email, :string
field :tentative_date, Ecto.Date
has_many :votes, WspApp.Vote
timestamps
end
end
Modelo - Vote
# /lib/wsp_app/vote.ex
defmodule WspApp.Vote do
use Ecto.Schema
import Ecto.Changeset
schema "votes" do
field :email, :string
belongs_to :workshop_proposal, WspApp.WorkshopProposal, foreign_key: :workshop_proposal_id
timestamps
end
end
Ecto.Changeset
● Filtran y hacen ‘cast’ de parametros externos.
● Provee mecanismos para dar seguimiento (tracking) y
validación de cambios.
WorkshopProposal Changeset v1
# /lib/wsp_app/workshop_proposal.ex
defmodule WspApp.WorkshopProposal do
use Ecto.Schema
import Ecto.Changeset
# ... schema
def changeset(workshop_proposal, params  %{}) do
workshop_proposal
|> cast(params, [:title, :description, :instructor_email, :tentative_date])
|> validate_required([:title, :instructor_email, :tentative_date])
end
end
WorkshopProposal Changeset v2
def changeset(workshop_proposal, params  %{}) do
workshop_proposal
|> cast(params, [:title, :description, :instructor_email, :tentative_date])
|> validate_required([:title, :instructor_email, :tentative_date])
|> validate_format(:instructor_email, ~r/@/)
|> validate_change(:tentative_date, fn(:tentative_date, tentative_date) ->
case Ecto.Date.compare(tentative_date, Ecto.Date.from_erl(:erlang.date())) do
:lt -> [tentative_date: "cannot be less than today"]
:gt -> []
:eq -> []
end
end)
end
Vote Changeset
# /lib/wsp_app/vote.ex
defmodule WspApp.Vote do
use Ecto.Schema
import Ecto.Changeset
# ...
def changeset(vote, params  %{}) do
vote
|> cast(params, [:email, :workshop_proposal_id])
|> validate_required([:email, :workshop_proposal_id])
|> validate_format(:email, ~r/@/)
end
end
Ecto.Changeset Struct
● Algunos campos son:
○ valid? - si el changeset es válido
○ data - la fuente de datos
○ changes - los cambios aplicados
○ errors - los errores después de las validaciones
Ecto.Query
● Consultas SQL escritas en Elixir
● Son seguras porque evitan problemas como SQL Injection
● Son “armables”
Consultas sencillas
import Ecto.Query
# SELECT * FROM workshop_proposal
WspApp.Repo.all(from w in WspApp.WorkshopProposal)
# SELECT id, title, instructor_email FROM workshop_proposal
WspApp.Repo.all(from w in WspApp.WorkshopProposal,
select: [w.id, w.title, w.instructor_email])
# SELECT * from workshop_proposal WHERE instructor_email = 'rgutierrez@nearsoft.com'
WspApp.Repo.all(from w in WspApp.WorkshopProposal,
where: w.instructor_email == "rgutierrez@nearsof.com")
Interpolacion y ‘Casting’
import Ecto.Query
date = Ecto.Date.cast!("2016-09-10")
WspApp.Repo.all(from w in WspApp.WorkshopProposal, where: w.tentative_date > ^date)
email = "rgutierrez@nearsoft.com"
WspApp.Repo.all(from w in WspApp.WorkshopProposal, where: w.instructor_email == ^email)
# Error: ** (Ecto.QueryError) iex:7: value `"0"` cannot be dumped to type :id.
WspApp.Repo.all(from w in WspApp.WorkshopProposal, where: w.id > "0")
Composicion
query = from w in WspApp.WorkshopProposal,
where: w.tentative_date > ^Ecto.Date.cast!("2016-09-10")
WspApp.Repo.all(query)
WspApp.Repo.all(from w in query, select: w.title)
WspApp.Repo.all(from w in query,
select: {w.title, w.instructor_email},
where: w.instructor_email not in ["rgutierrez@nearsoft.com"])
Paginación
WspApp.Repo.all(from w in WspApp.WorkshopProposal,
limit: 10,
offset: 0)
WspApp.Repo.all(from w in WspApp.WorkshopProposal,
limit: 10,
offset: 10)
Operadores de Agregación y Joins
WspApp.Repo.all(from w in WspApp.WorkshopProposal,
join: v in WspApp.Vote, on: v.workshop_proposal_id == w.id,
where: v.workshop_proposal_id == w.id,
select: {w.title, count(v.id)},
group_by: w.title)
# gracias a la asociación
WspApp.Repo.all(from w in WspApp.WorkshopProposal,
join: v in WspApp.Vote,
where: v.workshop_proposal_id == w.id,
select: {w.title, count(v.id)},
group_by: w.title)
● count, avg, sum, min, max, group_by, having, order_by
Fragments
● Cuando se requiere usar funciones/operaciones
específicas del manejador de base de datos.
from p in Post,
where: is_nil(p.published_at) and
fragment("downcase(?)", p.title) == ^title
startTime = Ecto.DateTime.cast!({{2016, 11, 11}, {18, 00, 00}})
endTime = Ecto.DateTime.cast!({{2016, 11, 11}, {20, 00, 00}})
from c in Workshop,
where: fragment("(?, ?) OVERLAPS (?, ?)",
c.start, c.end, type(^startTime, Ecto.DateTime), type(^endTime, Ecto.DateTime))
Gracias!!!
Q&A

Capa de persistencia con ecto

  • 1.
    Capa de Persistencia conEcto Rafael Gutiérrez, @abaddon_gtz Julio 23, 2016 @ElixirLangMx
  • 2.
    Ecto ● Framework depersistencia para Elixir (hecho en Elixir) ● Diseñado para bases de datos relacionales ● Ideas de LINQ de .NET y ActiveRecord de Rails ● Wrapper de la base de datos ● Lenguaje de consulta integrado (language integrated query - LIQ) !! ○ DSL
  • 3.
    LINQ & Ecto& SQL # Elixir from c in Customer, where: c.city == “Mexico”, select: c.company_name // C# from c in db.Customers where c.City == “Mexico” select c.CompanyName -- SQL SELECTcompany_name FROM customer WHERE city = ‘Mexico’
  • 4.
    Beneficios de unLIQ ● Metadatos ● Validación de sintaxis en tiempo de compilación ● Tipos ● Seguridad ● Composición
  • 5.
    Componentes de Ecto ●Ecto.Repo ● Ecto.Schema ● Ecto.Changeset ● Ecto.Query
  • 6.
    Caso de Estudio Administrarlas propuestas de Workshops
  • 7.
    Ecto.Repo ● Wrapper dela base de datos ● Via el Repo, creamos, consultamos, borramos y actualizamos los datos ● Usa un Adapter para comunicarse a la BD
  • 8.
    Crear el proyectocon mix ● > mix new wsp_app --sup ● --sup para generar una aplicación OTP con árbol de supervisión ● Ecto.Repo corre bajo un árbol de supervisión
  • 9.
    Dependencias # mix.exs defp depsdo [{:postgrex, ">= 0.0.0"}, {:ecto, "~> 2.0.0"}] end # mix.exs def application do [applications: [:logger, :ecto, :postgrex], mod: {WspApp, []}] end
  • 10.
    Repositorio y árbolde supervisión # Nuevo archivo /lib/wsp_app/repo.ex defmodule WspApp.Repo do use Ecto.Repo, otp_app: :wsp_app End # Agregar el repo al árbol de supervisión /lib/wsp_app.ex def start(_type, _args) do import Supervisor.Spec, warn: false children = [supervisor(WspApp.Repo, [])] opts = [strategy: :one_for_one, name: WspApp.Supervisor] Supervisor.start_link(children, opts) end
  • 11.
    Adapter # Configurar elAdapter /config/config.exs config :wsp_app, WspApp.Repo, adapter: Ecto.Adapters.Postgres, database: "ws_proposal_app_dev", username: "postgres", password: "", hostname: "localhost" # pool_size: 20 # desde Ecto 2 config :wsp_app, ecto_repos: [WspApp.Repo]
  • 12.
    Ecto.Schema ● Mapea lafuente de datos a un struct de Elixir ● Usado para mapear la tabla a datos de Elixir
  • 13.
    Migration - workshop_proposals #> mix ecto.gen.migration create_workshop_proposals_table # /priv/repo/migrations/ defmodule WspApp.Repo.Migrations.CreateWorkshopProposalsTable do use Ecto.Migration def change do create table(:workshop_proposals) do add :title, :text add :description, :text add :instructor_email, :text add :tentative_date, :date timestamps end end end
  • 14.
    Migration - votes #> mix ecto.gen.migration create_votes_table # /priv/repo/migrations/ defmodule WspApp.Repo.Migrations.CreateVotesTable do use Ecto.Migration def change do create table(:votes) do add :email, :text add :workshop_proposal_id, references(:workshop_proposals, [on_delete: :delete_all, on_update: :update_all]) timestamps end end end
  • 15.
    Migration - uniqueindex para votes # > mix ecto.gen.migration create_unique_vote_constraint # /priv/repo/migrations/ defmodule WspApp.Repo.Migrations.CreateUniqueVoteConstraint do use Ecto.Migration def change do create unique_index(:votes, [:email, :workshop_proposal_id]) end end
  • 16.
    Modelo - WorkshopProposal #/lib/wsp_app/workshop_proposal.ex defmodule WspApp.WorkshopProposal do use Ecto.Schema schema "workshop_proposals" do field :title, :string field :description, :string field :instructor_email, :string field :tentative_date, Ecto.Date has_many :votes, WspApp.Vote timestamps end end
  • 17.
    Modelo - Vote #/lib/wsp_app/vote.ex defmodule WspApp.Vote do use Ecto.Schema import Ecto.Changeset schema "votes" do field :email, :string belongs_to :workshop_proposal, WspApp.WorkshopProposal, foreign_key: :workshop_proposal_id timestamps end end
  • 18.
    Ecto.Changeset ● Filtran yhacen ‘cast’ de parametros externos. ● Provee mecanismos para dar seguimiento (tracking) y validación de cambios.
  • 19.
    WorkshopProposal Changeset v1 #/lib/wsp_app/workshop_proposal.ex defmodule WspApp.WorkshopProposal do use Ecto.Schema import Ecto.Changeset # ... schema def changeset(workshop_proposal, params %{}) do workshop_proposal |> cast(params, [:title, :description, :instructor_email, :tentative_date]) |> validate_required([:title, :instructor_email, :tentative_date]) end end
  • 20.
    WorkshopProposal Changeset v2 defchangeset(workshop_proposal, params %{}) do workshop_proposal |> cast(params, [:title, :description, :instructor_email, :tentative_date]) |> validate_required([:title, :instructor_email, :tentative_date]) |> validate_format(:instructor_email, ~r/@/) |> validate_change(:tentative_date, fn(:tentative_date, tentative_date) -> case Ecto.Date.compare(tentative_date, Ecto.Date.from_erl(:erlang.date())) do :lt -> [tentative_date: "cannot be less than today"] :gt -> [] :eq -> [] end end) end
  • 21.
    Vote Changeset # /lib/wsp_app/vote.ex defmoduleWspApp.Vote do use Ecto.Schema import Ecto.Changeset # ... def changeset(vote, params %{}) do vote |> cast(params, [:email, :workshop_proposal_id]) |> validate_required([:email, :workshop_proposal_id]) |> validate_format(:email, ~r/@/) end end
  • 22.
    Ecto.Changeset Struct ● Algunoscampos son: ○ valid? - si el changeset es válido ○ data - la fuente de datos ○ changes - los cambios aplicados ○ errors - los errores después de las validaciones
  • 23.
    Ecto.Query ● Consultas SQLescritas en Elixir ● Son seguras porque evitan problemas como SQL Injection ● Son “armables”
  • 24.
    Consultas sencillas import Ecto.Query #SELECT * FROM workshop_proposal WspApp.Repo.all(from w in WspApp.WorkshopProposal) # SELECT id, title, instructor_email FROM workshop_proposal WspApp.Repo.all(from w in WspApp.WorkshopProposal, select: [w.id, w.title, w.instructor_email]) # SELECT * from workshop_proposal WHERE instructor_email = 'rgutierrez@nearsoft.com' WspApp.Repo.all(from w in WspApp.WorkshopProposal, where: w.instructor_email == "rgutierrez@nearsof.com")
  • 25.
    Interpolacion y ‘Casting’ importEcto.Query date = Ecto.Date.cast!("2016-09-10") WspApp.Repo.all(from w in WspApp.WorkshopProposal, where: w.tentative_date > ^date) email = "rgutierrez@nearsoft.com" WspApp.Repo.all(from w in WspApp.WorkshopProposal, where: w.instructor_email == ^email) # Error: ** (Ecto.QueryError) iex:7: value `"0"` cannot be dumped to type :id. WspApp.Repo.all(from w in WspApp.WorkshopProposal, where: w.id > "0")
  • 26.
    Composicion query = fromw in WspApp.WorkshopProposal, where: w.tentative_date > ^Ecto.Date.cast!("2016-09-10") WspApp.Repo.all(query) WspApp.Repo.all(from w in query, select: w.title) WspApp.Repo.all(from w in query, select: {w.title, w.instructor_email}, where: w.instructor_email not in ["rgutierrez@nearsoft.com"])
  • 27.
    Paginación WspApp.Repo.all(from w inWspApp.WorkshopProposal, limit: 10, offset: 0) WspApp.Repo.all(from w in WspApp.WorkshopProposal, limit: 10, offset: 10)
  • 28.
    Operadores de Agregacióny Joins WspApp.Repo.all(from w in WspApp.WorkshopProposal, join: v in WspApp.Vote, on: v.workshop_proposal_id == w.id, where: v.workshop_proposal_id == w.id, select: {w.title, count(v.id)}, group_by: w.title) # gracias a la asociación WspApp.Repo.all(from w in WspApp.WorkshopProposal, join: v in WspApp.Vote, where: v.workshop_proposal_id == w.id, select: {w.title, count(v.id)}, group_by: w.title) ● count, avg, sum, min, max, group_by, having, order_by
  • 29.
    Fragments ● Cuando serequiere usar funciones/operaciones específicas del manejador de base de datos. from p in Post, where: is_nil(p.published_at) and fragment("downcase(?)", p.title) == ^title startTime = Ecto.DateTime.cast!({{2016, 11, 11}, {18, 00, 00}}) endTime = Ecto.DateTime.cast!({{2016, 11, 11}, {20, 00, 00}}) from c in Workshop, where: fragment("(?, ?) OVERLAPS (?, ?)", c.start, c.end, type(^startTime, Ecto.DateTime), type(^endTime, Ecto.DateTime))
  • 30.