What is Ecto?

Recently I have been playing with Ecto database abstraction framework. It is something similar to ORM (Object relational mapper) but the fact that there are no objects in Elixir makes this statement invalid. I wanted to share some awesome features of this framework and why I think it is a great tool to use.

The Model

Model is a central place to define for your data entities and validations on these entities. The model is really a lightweight struct and a module with functions to prepare data validations on it. Let's see an example on how the model actually looks like for an Entry that has an auto-generated slug and belongs to the user.

defmodule ElixirStream.Entry do
  use ElixirStream.Web, :model
  use Ecto.Model.Callbacks
  alias ElixirStream.User

  before_insert :set_slug

  schema "entries" do
    field :email, :string
    field :author_name, :string
    field :title, :string
    field :body, :string
    field :slug, :string
    belongs_to :user, User
    timestamps
  end

  @optional_fields ~w(email author_name)
  @required_fields ~w(title body)

  def changeset(model, params \\ :empty) do
    model
    |> cast(params, @required_fields, @optional_fields)
    |> validate_length(:title, min: 5)
    |> validate_length(:body, min: 15)
    |> validate_length(:body, max: 500)
  end

  def set_slug(changeset) do
    slug = Ecto.Changeset.get_field(changeset, :title) |>
    String.strip |> String.downcase |> String.replace(~r/([^a-z0-9])+/, "-")
    change(changeset, %{slug: slug})
  end
end

We define our schema with schema function that takes care of creating our data struct and validating this structure against the database. Also here we set our relations and types of the columns we need to have. These, of course, are casted in to acceptable Elixir data types.

In the changeset/2 function, we have an interesting bunch of validations and other stuff happening. We pass a name of our struct as an initial value and cast the parameters based on the required and optional parameters. The result of the cast/3 function is a changeset. (We will speak about the changesets in the next section) Afterwards, we just apply validations one by one by pipelining the functions one after another.

In this particular example, we have added callback functions to our model, that allow us to set slug before inserting the entry into the database. Even if you are not that familiar with Elixir it is really straightforward. We pass a changeset to set_slug/1 function and it gets field of title, then passes the title to String.strip/1, which in turn removes all the whitespace from the start and the end of the string. After that, we pass the result of this expression to String.downcase/1 function which is obvious. Afterwards, we replace each set of characters that are not a-z or 0-9 with a "-" string. After we done manipulating our slug to it's final form, we do an update to a changeset.

Changesets

We have to store the errors of the validations and keep track of the changes made to our entry models. Since we don't have objects, we can't just stick the errors and change tracking to our struct. Well, maybe we can, but it can get messy really quickly and we would go down the road of active record pretty fast, which is not the road that Ecto tries to follow (At least I think so). So the changeset is really a set of values that hold all the information that is needed for us. It's like a custom data type that has a dedicated functions in Ecto.Changeset module to apply any manipulations or updates on it. Check out the documentation on it.

The Repository and queries

Ecto creates a Repository for us automatically and it allows us to update, query, delete and insert records. The repository is a module that accepts changesets or structs to do the work with the database. Let's see an example of the fetching of the entries:

Repo.all from e in Entry, order_by: [desc: e.id], preload: [:user]

This looks quite interesting, in fact query language of ecto really is implemented in Elixir's macro system, so it looks like an SQL that complies with the language syntax rules. Check out all possible queries here.

So how about updates and inserts? Easy:

changeset = Entry.changeset(%Entry{}, params)
if changeset.valid? do
  Repo.insert(changeset)
  ## Oh the happy path
else
  ## The sad path :(
end

The semantics of insert and update are really not that different and you can figure out it on your own.

Conclusions

I really enjoy the separation of concerns in Ecto and the way it is structured. At first glance it looks too much like ActiveRecord of Rails, when in fact it is much much better isolated and is suitable to work with shoot load of data. So far I am loving it!

Janis Miezitis

Read more posts by this author.

Subscribe to Janis Miezitis personal blog

Get the latest posts delivered right to your inbox.

or subscribe via RSS with Feedly!