Do you want to pick up from where you left of?
Take me there

Changesets

Con el objetivo de insertar, actualizar o borrar datos de la base de datos, Ecto.Repo.insert/2, update/2 y delete/2 requieren un changeset como primer parámetro. Pero ¿Qué son los changesets?

Una tarea familiar para casi todo desarrollador es revisar la data de entrada en busca de potenciales errores - queremos asegurarnos que la data esté en un estado correcto, antes de intentar usarlos para nuestros propósitos.

Ecto provee una solución completa para trabajar con los cambios de datos en la forma de un módulo Changeset y una estructura de datos En esta lección vamos a explorar esta funcionalidad y aprender como verificar la integridad de la data antes de guardarla a la base de datos.

Creando tu primer changeset

Vamos a ver una estructura de %Changeset{} vacía:

iex> %Ecto.Changeset{}
%Ecto.Changeset<action: nil, changes: %{}, errors: [], data: nil, valid?: false>

Como puedes ver tiene algunos campos potencialmente útiles pero todos están vacíos.

Para que un changeset sea realmente útil cuando lo creamos necesitamos proveer una representación de como la data debería ser. ¿Pero qué mejor representación para nuestra data que los esquemas que creamos para definir campos y tipos?

Vamos a usar el esquema Friends.Person de la lección anterior:

defmodule Friends.Person do
  use Ecto.Schema

  schema "people" do
    field :name, :string
    field :age, :integer, default: 0
  end
end

Para crear un changeset usando el esquema Person vamos a usar Ecto.Changeset.cast/3:

iex> Ecto.Changeset.cast(%Friends.Person{name: "Bob"}, %{}, [:name, :age])
%Ecto.Changeset<action: nil, changes: %{}, errors: [], data: %Friends.Person<>,
 valid?: true>

El primer parámetro es la data original, una estructura inicial de %Friends.Person{} en este caso. Ecto es lo suficientemente inteligente para encontrar el esquema basándose en la estructura misma. El segundo parámetro son los cambios que queremos hacer, solo un mapa vació. El tercer parámetro es lo que hace a cast/3 especial: es una lista de los campos permitidos a usar los cuales nos dan la habilidad para controlar que campos pueden ser cambiados y mantener seguros al resto.

iex> Ecto.Changeset.cast(%Friends.Person{name: "Bob"}, %{"name" => "Jack"}, [:name, :age])
%Ecto.Changeset<
  action: nil,
  changes: %{name: "Jack"},
  errors: [],
  data: %Friends.Person<>,
  valid?: true
>

iex> Ecto.Changeset.cast(%Friends.Person{name: "Bob"}, %{"name" => "Jack"}, [])
%Ecto.Changeset<action: nil, changes: %{}, errors: [], data: %Friends.Person<>,
 valid?: true>

Puedes ver como el nuevo nombre fue ignorado en la segunda linea, donde este no fue explícitamente permitido.

Una alternativa a cast/3es la función change/2 la cual no tiene la habilidad de filtrar cambios como cast/3. Es útil cuando confías en la fuente que está haciendo los cambios o cuando trabajas con data manualmente.

Ahora podemos crear changesets pero dado que no tenemos validaciones cualquier cambio al nombre de la persona será aceptado, por lo que podríamos terminar con un nombre vació:

iex> Ecto.Changeset.change(%Friends.Person{name: "Bob"}, %{name: ""})
%Ecto.Changeset<
  action: nil,
  changes: %{name: nil},
  errors: [],
  data: %Friends.Person<>,
  valid?: true
>

Ecto dice que el changeset es válido pero realmente no queremos permitir valores vacíos. ¡Vamos a corregir eso!

Validaciones

Ecto viene con un número de funciones validadoras para ayudarnos.

Vamos a usar Ecto.Changeset mucho, entonces vamos a importar Ecto.Changeset en nuestro módulo person.ex el cual también contiene:

defmodule Friends.Person do
  use Ecto.Schema
  import Ecto.Changeset

  schema "people" do
    field :name, :string
    field :age, :integer, default: 0
  end
end

Ahora podemos usar la función cast/3 directamente.

Es común tener uno o mas funciones que creen changesets para un esquema. Vamos a hacer uno que acepte una estructura, un mapa de cambios y retorne un changeset:

def changeset(struct, params) do
  struct
  |> cast(params, [:name, :age])
end

Vamos a asegurarnos de que name esté siempre presente.

def changeset(struct, params) do
  struct
  |> cast(params, [:name, :age])
  |> validate_required([:name])
end

Cuando llamamos a la función Friends.Person.changeset/2 y le pasamos un nombre vació, el changeset ya no será válido e incluso contendrá un mensaje de error útil. Nota: no olvides correr recompile() cuando trabajes en iex de otro modo no reconocerá los cambio que hagas en el código.

iex> Friends.Person.changeset(%Friends.Person{}, %{"name" => ""})
%Ecto.Changeset<
  action: nil,
  changes: %{},
  errors: [name: {"can't be blank", [validation: :required]}],
  data: %Friends.Person<>,
  valid?: false
>

Si tratas de ejecutar Repo.insert(changeset) con el changeset de arriba recibirás una tupla {:error, changeset} con el mismo error por lo que no tienes que revisar changeset.valid? por ti mismo cada vez. Es fácil intentar hacer una inserción, actualización o eliminación y procesar el error luego si es que hay alguno.

Aparte de validate_required/2, existe también validate_length/3, pero toma algunas opciones extra:

def changeset(struct, params) do
  struct
  |> cast(params, [:name, :age])
  |> validate_required([:name])
  |> validate_length(:name, min: 2)
end

¡Puedes probarlo y adivinar cual sería el resultado si pasamos un nombre que consista de un simple carácter!

iex> Friends.Person.changeset(%Friends.Person{}, %{"name" => "A"})
%Ecto.Changeset<
  action: nil,
  changes: %{name: "A"},
  errors: [
    name: {"should be at least %{count} character(s)",
     [count: 2, validation: :length, kind: :min, type: :string]}
  ],
  data: %Friends.Person<>,
  valid?: false
>

Podrías estar sorprendido de que el mensaje de error contiene %{count} - esto es de ayuda para otros idiomas; si quieres mostrar los errores al usuario directamente puedes “humanizarlos” usando la función traverse_errors/2 — dale un vistazo al ejemplo provisto en la documentación.

Algunos de los validadores incluidos en Ecto.Changeset son:

Puedes encontrar la lista completa con detalles de como usarlos aquí.

Validadores personalizados

A pesar de que lo validadores incluidos cubren un amplio rango de casos de uso, aún puedes necesitas algo diferente.

Cada función validate_ que usamos hasta ahora acepta y regresa un %Ecto.Changeset{} por lo que podemos fácilmente insertar el nuestro.

Por ejemplo podemos asegurarnos de que solo nombres de personajes ficticios está permitidos:

@fictional_names ["Black Panther", "Wonder Woman", "Spiderman"]
def validate_fictional_name(changeset) do
  name = get_field(changeset, :name)

  if name in @fictional_names do
    changeset
  else
    add_error(changeset, :name, "is not a superhero")
  end
end

Arriba introdujimos dos nuevas funciones de ayuda get_field/3 y add_error/4. Lo que hacen casi que se explica por si mismo pero te recomiendo revisar los enlaces a la documentación.

Es una buena práctica siempre retornar un %Ecto.Changeset{} por lo que puedes usar el operador |> y hacer sencillo agregar mas validadores después:

def changeset(struct, params) do
  struct
  |> cast(params, [:name, :age])
  |> validate_required([:name])
  |> validate_length(:name, min: 2)
  |> validate_fictional_name()
end
iex> Friends.Person.changeset(%Friends.Person{}, %{"name" => "Bob"})
%Ecto.Changeset<
  action: nil,
  changes: %{name: "Bob"},
  errors: [name: {"is not a superhero", []}],
  data: %Friends.Person<>,
  valid?: false
>

¡Genial, funciona! Sin embargo no necesitamos implementar esta función por nosotros mismos, la función validadora validate_inclusion/4 podría ser usada en su lugar, puedes ver como puedes agregar tus propios errores lo cual puede ser útil.

Agregando cambios programáticamente

A veces quieres introducir cambios al changeset manualmente. la función put_change/3 existe para este propósito.

En lugar de hacer el campo name requerido vamos a permitir a los usuarios registrarse sin un nombre y los llamaremos “Anónimo”. La función que necesitamos lucirá familiar, acepta t regresa un changeset tal como la función validate_fictional_name/1 que hicimos antes:

def set_name_if_anonymous(changeset) do
  name = get_field(changeset, :name)

  if is_nil(name) do
    put_change(changeset, :name, "Anonymous")
  else
    changeset
  end
end

Podemos configurar el nombre de usuario como “Anónimo” solo cuando ellos se registren en nuestra aplicación, para hacer esto vamos a crear una nueva función creadora de changeset:

def registration_changeset(struct, params) do
  struct
  |> cast(params, [:name, :age])
  |> set_name_if_anonymous()
end

No tenemos que pasar un name y Anonymous debería ser una asignación automática:

iex> Friends.Person.registration_changeset(%Friends.Person{}, %{})
%Ecto.Changeset<
  action: nil,
  changes: %{name: "Anonymous"},
  errors: [],
  data: %Friends.Person<>,
  valid?: true
>

Teniendo funciones creadoras de changesets que tengan una responsabilidad específica (como registration_changeset/2) es común, a veces necesitas la flexibilidad de hacer solo ciertas validaciones o filtrar parámetros específicos. La función de arriba podría ser luego usada en una función de ayuda dedicada sign_up/1 en cualquier otro sitio:

def sign_up(params) do
  %Friends.Person{}
  |> Friends.Person.registration_changeset(params)
  |> Repo.insert()
end

Conclusión

Hay muchos casos de uso y funcionalidad que no hemos cubierto en esta lección tal como changesets sin esquema que puedes usar para validar cualquier data; o tratar con efecto colaterales a lo largo del changeset (prepare_changes/2 o trabajar con asociaciones y embebidos. Podríamos cubrir estos en una futura lección avanzada, pero por ahora te alentamos a explorar la documentación de Ecto Changeset para mas información.

¿Encontraste un error o quieres contribuir a la lección? ¡Edita esta lección en GitHub!