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.