ActiveRecord vs. Ecto Parte Dos

Esta es la segunda parte de la serie "ActiveRecord vs. Ecto", en la que Batman y Batgirl pelean por consultar bases de datos y comparamos manzanas y naranjas.

Después de analizar los esquemas y las migraciones de la base de datos en ActiveRecord vs.Ecto parte uno, esta publicación cubre cómo ActiveRecord y Ecto permiten a los desarrolladores consultar la base de datos, y cómo se comparan ActiveRecord y Ecto cuando se enfrentan a los mismos requisitos. En el camino, también descubriremos la identidad de Batgirl 1989-2011.

Datos de semillas

¡Empecemos! Según la estructura de la base de datos definida en la primera publicación de esta serie, suponga que los usuarios y las tablas de facturas tienen almacenados los siguientes datos:

los usuarios

* El campo created_at de ActiveRecord se llama insert_at en Ecto de forma predeterminada.

facturas

* El campo created_at de ActiveRecord se llama insert_at en Ecto de forma predeterminada.

Las consultas realizadas a través de esta publicación suponen que los datos anteriores se almacenan en la base de datos, por lo tanto, tenga en cuenta esta información mientras la lee.

Encuentra un artículo usando su clave principal

Comencemos por obtener un registro de la base de datos utilizando su clave principal.

ActiveRecord

irb (main): 001: 0> User.find (1) User Load (0.4ms) SELECCIONE "usuarios". * DESDE "usuarios" DONDE "usuarios". "id" = $ 1 LÍMITE $ 2 [["id", 1 ], ["LIMIT", 1]] => # 

Ecto

iex (3)> Repo.get (Usuario, 1)
[debug] QUERY OK source = "users" db = 5.2ms decode = 2.5ms queue = 0.1ms
SELECCIONE u0. "Id", u0. "Full_name", u0. "Email", u0. "Insert_at", u0. "Updated_at" FROM "users" AS u0 WHERE (u0. "Id" = $ 1) [1]
% Financex.Accounts.User {
  __meta__: # Ecto.Schema.Metadata <: loaded, "users">,
  correo electrónico: "[email protected]",
  nombre completo: "Bette Kane",
  id: 1
  insert_at: ~ N [2018-01-01 10: 01: 00.000000],
  facturas: # Ecto.Association.NotLoaded ,
  updated_at: ~ N [2018-01-01 10: 01: 00.000000]
}

Comparación

Ambos casos son bastante similares. ActiveRecord se basa en el método de buscar clase de la clase de modelo Usuario. Significa que cada clase secundaria ActiveRecord tiene su propio método de búsqueda.

Ecto utiliza un enfoque diferente, basándose en el concepto de repositorio como mediador entre la capa de mapeo y el dominio. Cuando se usa Ecto, el módulo Usuario no tiene conocimiento sobre cómo encontrarse a sí mismo. Dicha responsabilidad está presente en el módulo Repo, que puede asignarlo al almacén de datos debajo, que en nuestro caso es Postgres.

Al comparar la consulta SQL en sí, podemos detectar algunas diferencias:

  • ActiveRecord carga todos los campos (usuarios. *), Mientras que Ecto carga solo los campos enumerados en la definición del esquema.
  • ActiveRecord incluye un LÍMITE 1 a la consulta, mientras que Ecto no.

Obteniendo todos los artículos

Avancemos un paso más y carguemos todos los usuarios de la base de datos.

ActiveRecord

irb (main): 001: 0> User.all User Load (0.5ms) SELECT "users". * FROM "users" LIMIT $ 1 [["LIMIT", 11]] => # , # , # , # ]>

Ecto

iex (4)> Repo.all (Usuario)
[debug] QUERY OK source = "users" db = 2.8ms decode = 0.2ms queue = 0.2ms
SELECCIONE u0. "Id", u0. "Full_name", u0. "Email", u0. "Insert_at", u0. "Updated_at" FROM "users" AS u0 []
[
  % Financex.Accounts.User {
    __meta__: # Ecto.Schema.Metadata <: loaded, "users">,
    correo electrónico: "[email protected]",
    nombre completo: "Bette Kane",
    id: 1
    insert_at: ~ N [2018-01-01 10: 01: 00.000000],
    facturas: # Ecto.Association.NotLoaded ,
    updated_at: ~ N [2018-01-01 10: 01: 00.000000]
  },
  % Financex.Accounts.User {
    __meta__: # Ecto.Schema.Metadata <: loaded, "users">,
    correo electrónico: "[email protected]",
    nombre completo: "Barbara Gordon",
    id: 2
    insert_at: ~ N [2018-01-02 10: 02: 00.000000],
    facturas: # Ecto.Association.NotLoaded ,
    updated_at: ~ N [2018-01-02 10: 02: 00.000000]
  },
  % Financex.Accounts.User {
    __meta__: # Ecto.Schema.Metadata <: loaded, "users">,
    correo electrónico: "[email protected]",
    nombre completo: "Cassandra Cain",
    id: 3
    insert_at: ~ N [2018-01-03 10: 03: 00.000000],
    facturas: # Ecto.Association.NotLoaded ,
    updated_at: ~ N [2018-01-03 10: 03: 00.000000]
  },
  % Financex.Accounts.User {
    __meta__: # Ecto.Schema.Metadata <: loaded, "users">,
    correo electrónico: "[email protected]",
    nombre_completo: "Stephanie Brown",
    id: 4
    insert_at: ~ N [2018-01-04 10: 04: 00.000000],
    facturas: # Ecto.Association.NotLoaded ,
    updated_at: ~ N [2018-01-04 10: 04: 00.000000]
  }
]

Comparación

Sigue exactamente el mismo patrón que la sección anterior. ActiveRecord utiliza el método de todas las clases y Ecto se basa en el patrón de repositorio para cargar los registros.

De nuevo hay algunas diferencias en las consultas SQL:

Consultar con condiciones

Es muy poco probable que necesitemos obtener todos los registros de una tabla. Una necesidad común es el uso de condiciones, para filtrar los datos devueltos.

Usemos ese ejemplo para enumerar todas las facturas que aún deben pagarse (WHERE paid_at IS NULL).

ActiveRecord

irb (main): 024: 0> Invoice.where (paid_at: nil) Factura Load (18.2ms) SELECCIONE "facturas". * DESDE "facturas" DONDE "facturas". "paid_at" ES LÍMITE NULO $ 1 [["LÍMITE" , 11]] => # , # ]>

Ecto

iex (19)> where (Factura, [i], is_nil (i.paid_at)) |> Repo.all ()
[debug] QUERY OK source = "facturas" db = 20.2ms
SELECCIONE i0. "Id", i0. "Payment_method", i0. "Paid_at", i0. "User_id", i0. "Insert_at", i0. "Updated_at" FROM "factures" AS i0 WHERE (i0. "Paid_at" IS NULO) []
[
  % Financex.Accounts.Invoice {
    __meta__: # Ecto.Schema.Metadata <: cargado, "facturas">,
    id: 3
    insert_at: ~ N [2018-01-04 08: 00: 00.000000],
    paid_at: nil,
    método_pago: nulo
    updated_at: ~ N [2018-01-04 08: 00: 00.000000],
    usuario: # Ecto.Association.NotLoaded ,
    user_id: 3
  },
  % Financex.Accounts.Invoice {
    __meta__: # Ecto.Schema.Metadata <: cargado, "facturas">,
    id: 4
    insert_at: ~ N [2018-01-04 08: 00: 00.000000],
    paid_at: nil,
    método_pago: nulo
    updated_at: ~ N [2018-01-04 08: 00: 00.000000],
    usuario: # Ecto.Association.NotLoaded ,
    user_id: 4
  }
]

Comparación

En ambos ejemplos, se utiliza la palabra clave where, que es una conexión con la cláusula WHERE de SQL. Aunque las consultas SQL generadas son bastante similares, la forma en que ambas herramientas llegan allí tiene algunas diferencias importantes.

ActiveRecord transforma el argumento paid_at: nil en la instrucción SQL IS_ULL NULL paid_at automáticamente. Para llegar a la misma salida usando Ecto, los desarrolladores deben ser más explícitos sobre su intención, llamando a is_nil ().

Otra diferencia a destacar es el comportamiento "puro" de la función en Ecto. Cuando se llama solo a la función where, no interactúa con la base de datos. El retorno de la función where es una estructura Ecto.Query:

iex (20)> where (Factura, [i], is_nil (i.paid_at))
# Ecto.Query 

La base de datos solo se toca cuando se llama a la función Repo.all (), pasando la estructura Ecto.Query como argumento. Este enfoque permite la composición de consultas en Ecto, que es el tema de la siguiente sección.

Composición de la consulta

Uno de los aspectos más poderosos de las consultas de bases de datos es la composición. Describe una consulta de una manera que contiene más de una condición.

Si está creando consultas SQL sin procesar, significa que probablemente usará algún tipo de concatenación. Imagina que tienes dos condiciones:

  1. not_paid = 'paid_at NO ES NULO'
  2. paid_with_paypal = 'payment_method = "Paypal"'

Para combinar esas dos condiciones usando SQL sin procesar, significa que tendrás que concatenarlas usando algo similar a:

SELECCIONE * DE las facturas DONDE # {not_paid} Y # {paid_with_paypal}

Afortunadamente, tanto ActiveRecord como Ecto tienen una solución para eso.

ActiveRecord

irb (main): 003: 0> Invoice.where.not (paid_at: nil) .where (payment_method: "Paypal") Invoice Load (8.0ms) SELECCIONE "facturas". * DESDE "facturas" DONDE "facturas". " paid_at "NO ES NULO Y" facturas "." payment_method "= $ 1 LIMIT $ 2 [[" payment_method "," Paypal "], [" LIMIT ", 11]] => # ]>

Ecto

iex (6)> Factura |> where ([i], not is_nil (i.paid_at)) |> where ([i], i.payment_method == "Paypal") |> Repo.all ()
[debug] QUERY OK source = "facturas" db = 30.0ms decodificación = 0.6ms cola = 0.2ms
SELECCIONE i0. "Id", i0. "Payment_method", i0. "Paid_at", i0. "User_id", i0. "Insert_at", i0. "Updated_at" FROM "factures" AS i0 WHERE (NOT (i0. "Paid_at "IS NULL)) AND (i0." Payment_method "= 'Paypal') []
[
  % Financex.Accounts.Invoice {
    __meta__: # Ecto.Schema.Metadata <: cargado, "facturas">,
    id: 2
    insert_at: ~ N [2018-01-03 08: 00: 00.000000],
    paid_at: #DateTime <2018-02-01 08: 00: 00.000000Z>,
    método_pago: "Paypal",
    updated_at: ~ N [2018-01-03 08: 00: 00.000000],
    usuario: # Ecto.Association.NotLoaded ,
    user_id: 2
  }
]

Comparación

Ambas consultas responden a la misma pregunta: "¿Qué facturas se pagaron y utilizaron Paypal?".

Como ya se esperaba, ActiveRecord ofrece una forma más sucinta de componer la consulta (para ese ejemplo), mientras que Ecto requiere que los desarrolladores gasten un poco más en escribir la consulta. Como de costumbre, Batgirl (la huérfana, muda con la identidad de Cassandra Caín) o Activerecord no es tan detallada.

No se deje engañar por la verbosidad y la aparente complejidad de la consulta Ecto que se muestra arriba. En un entorno del mundo real, esa consulta se volvería a escribir para que se parezca más a:

Factura
|> where ([i], no es_nil (i.paid_at))
|> where ([i], i.payment_method == "Paypal")
|> Repo.all ()

Visto desde ese ángulo, la combinación de los aspectos "puros" de la función donde, que no realiza operaciones de base de datos por sí misma, con el operador de tubería, hace que la composición de la consulta en Ecto sea realmente limpia.

Ordenar

Ordenar es un aspecto importante de una consulta. Permite a los desarrolladores asegurarse de que el resultado de una consulta determinada siga un orden específico.

ActiveRecord

irb (main): 002: 0> Invoice.order (created_at:: desc) Invoice Load (1.5ms) SELECCIONE "facturas". * DESDE "facturas" ORDENAR POR "facturas". "created_at" LÍMITE DE DESC. ", 11]] => # , # , # , # ]>

Ecto

iex (6)> order_by (Factura, desc:: insert_at) |> Repo.all ()
[debug] QUERY OK source = "facturas" db = 19.8ms
SELECCIONE i0. "Id", i0. "Payment_method", i0. "Paid_at", i0. "User_id", i0. "Insert_at", i0. "Updated_at" FROM "factures" AS i0 ORDER BY i0. "Insert_at" DESC []
[
  % Financex.Accounts.Invoice {
    __meta__: # Ecto.Schema.Metadata <: cargado, "facturas">,
    id: 3
    insert_at: ~ N [2018-01-04 08: 00: 00.000000],
    paid_at: nil,
    método_pago: nulo
    updated_at: ~ N [2018-01-04 08: 00: 00.000000],
    usuario: # Ecto.Association.NotLoaded ,
    user_id: 3
  },
  % Financex.Accounts.Invoice {
    __meta__: # Ecto.Schema.Metadata <: cargado, "facturas">,
    id: 4
    insert_at: ~ N [2018-01-04 08: 00: 00.000000],
    paid_at: nil,
    método_pago: nulo
    updated_at: ~ N [2018-01-04 08: 00: 00.000000],
    usuario: # Ecto.Association.NotLoaded ,
    user_id: 4
  },
  % Financex.Accounts.Invoice {
    __meta__: # Ecto.Schema.Metadata <: cargado, "facturas">,
    id: 2
    insert_at: ~ N [2018-01-03 08: 00: 00.000000],
    paid_at: #DateTime <2018-02-01 08: 00: 00.000000Z>,
    método_pago: "Paypal",
    updated_at: ~ N [2018-01-03 08: 00: 00.000000],
    usuario: # Ecto.Association.NotLoaded ,
    user_id: 2
  },
  % Financex.Accounts.Invoice {
    __meta__: # Ecto.Schema.Metadata <: cargado, "facturas">,
    id: 1
    insert_at: ~ N [2018-01-02 08: 00: 00.000000],
    paid_at: #DateTime <2018-02-01 08: 00: 00.000000Z>,
    método_pago: "Tarjeta de crédito",
    updated_at: ~ N [2018-01-02 08: 00: 00.000000],
    usuario: # Ecto.Association.NotLoaded ,
    user_id: 1
  }
]

Comparación

Agregar orden a una consulta es sencillo en ambas herramientas.

Aunque el ejemplo de Ecto usa una factura como primer parámetro, la función order_by también acepta estructuras Ecto.Query, lo que permite que la función order_by se use en composiciones, como:

Factura
|> where ([i], no es_nil (i.paid_at))
|> where ([i], i.payment_method == "Paypal")
|> order_by (desc:: insert_at)
|> Repo.all ()

Limitando

¿Qué sería una base de datos sin límite? Un desastre. Afortunadamente, tanto ActiveRecord como Ecto ayudan a limitar la cantidad de registros devueltos.

ActiveRecord

irb (main): 004: 0> Invoice.limit (2)
Carga de factura (0.2ms) SELECCIONE "facturas". * DESDE "LÍMITES" LÍMITE $ 1 [["LÍMITE", 2]]
=> # , # ]>

Ecto

iex (22)> límite (Factura, 2) |> Repo.all ()
[debug] QUERY OK source = "facturas" db = 3.6ms
SELECCIONE i0. "Id", i0. "Método_pago", i0. "Pagado_en", i0. "Id_usuario", i0. "Insertadoat", i0. "Actualizado_en" DE "facturas" COMO i0 LÍMITE 2 []
[
  % Financex.Accounts.Invoice {
    __meta__: # Ecto.Schema.Metadata <: cargado, "facturas">,
    id: 1
    insert_at: ~ N [2018-01-02 08: 00: 00.000000],
    paid_at: #DateTime <2018-02-01 08: 00: 00.000000Z>,
    método_pago: "Tarjeta de crédito",
    updated_at: ~ N [2018-01-02 08: 00: 00.000000],
    usuario: # Ecto.Association.NotLoaded ,
    user_id: 1
  },
  % Financex.Accounts.Invoice {
    __meta__: # Ecto.Schema.Metadata <: cargado, "facturas">,
    id: 2
    insert_at: ~ N [2018-01-03 08: 00: 00.000000],
    paid_at: #DateTime <2018-02-01 08: 00: 00.000000Z>,
    método_pago: "Paypal",
    updated_at: ~ N [2018-01-03 08: 00: 00.000000],
    usuario: # Ecto.Association.NotLoaded ,
    user_id: 2
  }
]

Comparación

Tanto ActiveRecord como Ecto tienen una forma de limitar el número de registros devueltos por una consulta.

El límite de Ecto funciona de manera similar a order_by, siendo adecuado para composiciones de consulta.

Asociaciones

ActiveRecord y Ecto tienen diferentes enfoques cuando se trata de cómo se manejan las asociaciones.

ActiveRecord

En ActiveRecord, puede usar cualquier asociación definida en un modelo, sin tener que hacer nada especial al respecto, por ejemplo:

irb (main): 012: 0> user = User.find (2) User Load (0.3ms) SELECT "users". * FROM "users" WHERE "users". "id" = $ 1 LIMIT $ 2 [["id" , 2], ["LIMIT", 1]] => #  irb (main): 013: 0> user.invoices Carga de facturas (0.4ms) SELECCIONE" facturas ". * DESDE" facturas "DONDE" facturas " . "user_id" = $ 1 LIMIT $ 2 [["user_id", 2], ["LIMIT", 11]] => # ] >

El ejemplo anterior muestra que podemos obtener una lista de las facturas de los usuarios al llamar a user.invoices. Al hacerlo, ActiveRecord consultó automáticamente la base de datos y cargó las facturas asociadas con el usuario. Si bien este enfoque facilita las cosas, en el sentido de escribir menos código o tener que preocuparse por pasos adicionales, podría ser un problema si está iterando sobre varios usuarios y obteniendo las facturas para cada usuario. Este problema se conoce como el "problema N + 1".

En ActiveRecord, la solución propuesta para el "problema N + 1" es utilizar el método de inclusión:

irb (main): 022: 0> user = User.includes (: factures) .find (2) User Load (0.3ms) SELECT "users". * FROM "users" WHERE "users". "id" = $ 1 LIMIT $ 2 [["id", 2], ["LIMIT", 1]] Carga de facturas (0.6ms) SELECCIONE "facturas". * DESDE "facturas" DONDE "facturas". "User_id" = $ 1 [["user_id", 2]] => #  irb (main): 023: 0> user.invoices => # ]>

En este caso, ActiveRecord carga con entusiasmo la asociación de facturas al buscar al usuario (como se ve en las dos consultas SQL que se muestran).

Ecto

Como ya habrás notado, a Ecto realmente no le gusta la magia o la implicidad. Requiere que los desarrolladores sean explícitos sobre sus intenciones.

Probemos el mismo enfoque de usar user.invoices con Ecto:

iex (7)> ​​usuario = Repo.get (Usuario, 2)
[debug] QUERY OK source = "users" db = 18.3ms decode = 0.6ms
SELECCIONE u0. "Id", u0. "Full_name", u0. "Email", u0. "Insert_at", u0. "Updated_at" FROM "users" AS u0 WHERE (u0. "Id" = $ 1) [2]
% Financex.Accounts.User {
  __meta__: # Ecto.Schema.Metadata <: loaded, "users">,
  correo electrónico: "[email protected]",
  nombre completo: "Barbara Gordon",
  id: 2
  insert_at: ~ N [2018-01-02 10: 02: 00.000000],
  facturas: # Ecto.Association.NotLoaded ,
  updated_at: ~ N [2018-01-02 10: 02: 00.000000]
}
iex (8)> user.invoices
# Ecto.Association.NotLoaded 

El resultado es un Ecto.Association.NotLoaded. No tan útil

Para tener acceso a las facturas, un desarrollador debe informar a Ecto sobre eso, utilizando la función de precarga:

iex (12)> usuario = precarga (Usuario,: ​​facturas) |> Repo.get (2)
[debug] QUERY OK source = "users" db = 11.8ms
SELECCIONE u0. "Id", u0. "Full_name", u0. "Email", u0. "Insert_at", u0. "Updated_at" FROM "users" AS u0 WHERE (u0. "Id" = $ 1) [2]
[debug] QUERY OK source = "facturas" db = 4.2ms
SELECCIONE i0. "Id", i0. "Método_pago", i0. "Pagado_at", i0. "Id_usuario", i0. "Insertadoat", i0. "Actualizado_at", i0. "Id_usuario" DE "facturas" COMO i0 DONDE ( i0. "user_id" = $ 1) ORDENADO POR i0. "user_id" [2]
% Financex.Accounts.User {
  __meta__: # Ecto.Schema.Metadata <: loaded, "users">,
  correo electrónico: "[email protected]",
  nombre completo: "Barbara Gordon",
  id: 2
  insert_at: ~ N [2018-01-02 10: 02: 00.000000],
  facturas: [
    % Financex.Accounts.Invoice {
      __meta__: # Ecto.Schema.Metadata <: cargado, "facturas">,
      id: 2
      insert_at: ~ N [2018-01-03 08: 00: 00.000000],
      paid_at: #DateTime <2018-02-01 08: 00: 00.000000Z>,
      método_pago: "Paypal",
      updated_at: ~ N [2018-01-03 08: 00: 00.000000],
      usuario: # Ecto.Association.NotLoaded ,
      user_id: 2
    }
  ],
  updated_at: ~ N [2018-01-02 10: 02: 00.000000]
}

iex (15)> facturas de usuario
[
  % Financex.Accounts.Invoice {
    __meta__: # Ecto.Schema.Metadata <: cargado, "facturas">,
    id: 2
    insert_at: ~ N [2018-01-03 08: 00: 00.000000],
    paid_at: #DateTime <2018-02-01 08: 00: 00.000000Z>,
    método_pago: "Paypal",
    updated_at: ~ N [2018-01-03 08: 00: 00.000000],
    usuario: # Ecto.Association.NotLoaded ,
    user_id: 2
  }
]

De manera similar a ActiveRecord, incluye la precarga con la obtención de las facturas asociadas, que las harán disponibles al llamar a user.invoices.

Comparación

Una vez más, la batalla entre ActiveRecord y Ecto termina con un punto conocido: lo explícito. Ambas herramientas permiten a los desarrolladores acceder fácilmente a las asociaciones, pero mientras ActiveRecord lo hace menos detallado, el resultado podría tener comportamientos inesperados. Ecto sigue el tipo de enfoque WYSIWYG, que solo hace lo que se ve en la consulta definida por el desarrollador.

Rails es conocido por usar y promover estrategias de almacenamiento en caché en todas las diferentes capas de la aplicación. Un ejemplo es sobre el uso del enfoque de almacenamiento en caché de "muñeca rusa", que se basa completamente en el "problema N + 1" para que su mecanismo de almacenamiento en caché realice su magia.

Validaciones

La mayoría de las validaciones presentes en ActiveRecord también están disponibles en Ecto. Aquí hay una lista de validaciones comunes y cómo ActiveRecord y Ecto las definen:

Envolver

Ahí lo tienes: la comparación esencial entre manzanas y naranjas.

ActiveRecord se centra en la facilidad de realizar consultas a la base de datos. La gran mayoría de sus características se concentran en las clases de modelos en sí, y no requieren que los desarrolladores tengan una comprensión profunda de la base de datos, ni el impacto de tales operaciones. ActiveRecord hace muchas cosas implícitamente por defecto. Aunque eso hace que sea más fácil comenzar, hace que sea más difícil entender lo que está sucediendo detrás de escena y solo funciona si sigues el "modo ActiveRecord".

Ecto, por otro lado, requiere explícitamente lo que resulta en un código más detallado. Como beneficio, todo está en el centro de atención, nada detrás de escena, y puede especificar su propio camino.

Ambos tienen su lado positivo dependiendo de su perspectiva y preferencia. Luego de haber comparado las manzanas y las naranjas, llegamos al final de este BAT-tle. Casi olvidé decirte que el nombre en clave de BatGirl (1989–2001) era ... Oráculo. Pero no entremos en eso.

Esta publicación está escrita por el autor invitado Elvio Vicosa. Elvio es el autor del libro Phoenix for Rails Developers.

Publicado originalmente en blog.appsignal.com el 9 de octubre de 2018.