Note: This article requires basic knowledge of elixir. If you are not familiar with I will encourage you to check the link
Ecto is a database abstraction layer for elixir. It provides a domain specific language to interacting with your database. It is also intended for relational database much like Active record. But it share more traits with LINQ in how it generate queries. Ecto in many ways different from ORMs like Active record. Unlike Active record, which uses model as a direct interface to interact with database, Ecto has separate concern for querying (using Query module) and actually perform the CRUD operation(using Repo module). Ecto has four components. Schema, Query, Changeset and Repo for handling different concerns.
We will use Ecto with Phoenix framework. So let's take an example of a simple blog application, we first need to create a Schema for Post
#web/models/post.ex
defmodule Blog.Post do
use Blog.Web, :model
schema :posts do
field :title, :string
field :content, :string
timestamps()
end
end
Here, in the first statement by using Web model it will import Ecto.Changeset and Ecto.Query and use Schema. This will bring set of functions and macros in the model which we can use for our advantage.
# web/web.ex
defmodule Blog.Web do
def model do
quote do
use Ecto.Schema
import Ecto
import Ecto.Changeset
import Ecto.Query
end
end
end
Ecto.Changeset provides methods for casting params, provide validation and track the changes.
Ecto.Query has various methods to generate query objects which can be used in combinations.
Changeset
A changeset is an elixir struct that contains changes of what should be modified in database. Generally we add a changeset
method in our model that take care of casting external params, validation, adding any constraint and put any change. To create a changeset we use Ecto.Changeset.cast/3
method to cast items from a map
#web/models/post.ex
defmodule Blog.Post do
use Blog.Web, :model
schema :posts do
field :title, :string
field :content, :string
timestamps()
end
def changeset(struct, params // %{}) do
struct
|> cast(params, [:title, :content])
end
end
Blog.Post is a type of elixir struct that interact with the database. We can create a record by using an empty Blog.Post struct as a first argument, params map as second argument to this Blog.Post.changeset/2
method. This method will return an Ecto changeset object
alias Blog.Post
%Post{}
|> Post.changeset(%{title: "React for beginner", content: "Bla bla bla...."})
It will return a changeset object which contains the changes, what action perform insert/update/delete, also track if the changes are valid to update in the database.
#Ecto.Changeset<action: nil, changes: %{title: "React for beginner", content: "Bla bla bla...."}, errors: [],
data: #Blog.Post<>, valid?: true>
Ecto has a better approach in handling changes and validation. In Active record or any such ORM it handle validations one for all approach it means it assume that the validations written on model will be universally used. If you dont want a validation in some scenario you need to dig down to change the code, which will be very hactic as the application scales. Ecto takes smarter approach to create different changesets depending on the use case. Take an example of User signup when he first signs up we need to add email, password contains sensative information and another changeset for updating the profile information such as first_name, last_name, profile pic etc. In the first changeset we only added one validation which checks if nickname is present otherwise throws a validation error. However registration changeset contains more sensetive information. We need to encrypt the password before saving to database we have added a private method and used Comonin.Bcrypt.hashpwsalt/1
for password encryption. get_change and put_chnage are methods provided by Ecto.Changeset
which fetch and put any value for a given key respectively.
# web/models/user/ex
defmodule Blog.User do
....
def changeset(strcut, params \\ %{}) do
changeset = strcut
|> cast(params, [:first_name, :last_name, :nickname])
|> validate_required([:nickname])
end
def registration_changeset(struct, params \\ %{}) do
struct
|> cast(params, [:email, :password])
|> validate_required([:email, :password])
|> validate_length(:password, min: 8)
|> generate_hash_password
end
...
defp generate_hash_password(changeset) do
hash = get_change(changeset, :password) |> Comeonin.Bcrypt.hashpwsalt
changeset |> put_change(:password_hash, hash)
end
end
To insert a user
alias Blog.User
params = %{email: "punit.jain@kiprosh.com", password: "12345678"}
changeset = %User{} |> User.registration_changeset(params)
changeset |> Blog.Repo.insert
Here you can see that we just need to pass the changeset
to insert/1
method and not passing any model name because changeset track the information of model and changes.