One of the functions added to Ecto 2.0 is Ecto.Repo.insert_all/3 . insert_all allows developers to insert multiple entries at once into a repository:

MyApp.Repo.insert_all(Post, [[title: "hello", body: "world"], [title: "another", body: "post"]])

Although insert_all is just a regular Elixir function, it plays an important role in Ecto 2.0 goals. To understand more about these goals, let’s talk about Ecto schemas.

The trouble with schemas

Conceptually speaking, Ecto 2.0 is quite different from Ecto 1.0 as it moves to a more data-oriented approach. We want developers to think of Ecto as a tool instead of their domain layer. One important decision in this direction was the removal of Ecto.Model in favor of Ecto.Schema .

At this point, it is worth asking: what are schemas?

Ecto schemas are used to map any data source into an Elixir struct. Schemas are useful because they give shape to external data and enforce its types:

defmodule Post do use Ecto.Schema schema "posts" do field :title field :body field :votes, :integer, default: 0 timestamps end end

One possible benefit of using schemas is that you define the shape of the data once and you can use this shape to retrieve data from the database as well as coordinate changes happening on the data. For example:

params = [title: "new title", votes: "0"] Post |> MyApp.Repo.get!(13) |> Ecto.Changeset.cast(params, [:title, :votes]) |> MyApp.Repo.update!

By relying on the schema information, Ecto knows the shape of the data when it reads from the database and know how to manage changes. In the example above, the “votes” field was automatically cast from string to an integer based on its schema type.

While the benefits of schemas are known, we don’t talk as frequently about the downsides of schemas: which is exactly the coupling of the database representation to your application, leading developers to represent both reads and writes operations on top of the same structure.

With schemaless queries, we get direct access to all underlying database operations, allowing us to perform both reads and writes operations without being coupled to a schema.

Schemaless queries

Ecto 2.0 allows read, create, update and delete operations to be done without a schema. insert_all was the last piece of the puzzle. Let’s see some examples.

If you are writing a reporting view, it may be counter-productive to think how your existing application schemas relate to the report being generated. It is often simpler to write a query that returns only the data you need, without taking schemas into account:

import Ecto.Query def running_activities(start_at, end_at) MyApp.Repo.all( from u in "users", join: a in "activities", on: a.user_id == u.id, where: a.start_at > type(^start_at, Ecto.DateTime) and a.end_at < type(^end_at, Ecto.DateTime), group_by: a.user_id, select: %{user_id: a.user_id, interval: a.start_at - a.end_at, count: count(u.id)} ) end

The function above does not care about your schemas. It returns only the data that matters for building the report. Notice how we use the type/2 function to specify what is the expected type of the argument we are interpolating, allowing us to benefit from the same type casting guarantees a schema would give.

Inserts, updates and deletes can also be done without schemas via insert_all , update_all and delete_all respectively:

# Insert data into posts and return its ID [%{id: id}] = MyApp.Repo.insert_all "posts", [[title: "hello"]], returning: [:id] # Use the ID to trigger updates post = from p in "posts", where: p.id == ^id {1, _} = MyApp.Repo.update_all post, set: [title: "new title"] # As well as for deletes {1, _} = MyApp.Repo.delete_all post

It is not hard to see how these operations directly map to their SQL variants, keeping the database at your fingertips without the need to intermediate all operations through schemas.

Schemas are mappers

When we defined schemas above, we said:

Ecto schemas are used to map any data source into an Elixir struct.

We put emphasis on any because it is a common misconception to think Ecto schemas map only to your database tables.

For instance, when you write a web application using Phoenix and you use Ecto to receive external changes and apply such changes to your database, we are actually mapping the schema to two different sources:

Database <-> Ecto schema <-> Forms / API

It is important to understand how the schema is sitting between your database and your API because in many situations it is better to break this mapping in two. Let’s see some practical examples.

Imagine you are working with a client that wants the “Sign Up” form to contain the fields “First name”, “Last name” along side “E-mail” and other information. You know there are a couple problems with this approach.

First of all, not everyone has a first and last name. Although your client is decided on presenting both fields, they are a UI concern, and you don’t want the UI to dictate the shape of your data. Furthermore, you know it would be useful to break the “Sign Up” information across two tables, the “accounts” and “profiles” tables.

Given the requirements above, how would we implement the Sign Up feature in the backend?

One approach would be to have two schemas, Account and Profile, with virtual fields such as first_name and last_name , and use associations along side nested forms to tie the schemas to your UI. One of such schemas would be:

defmodule Profile do use Ecto.Schema schema "profiles" do field :name field :first_name, :string, virtual: true field :last_name, :string, virtual: true ... end end

It is not hard to see how we are polluting our Profile schema with UI requirements by adding fields such first_name and last_name . If the Profile schema is used for both reading and writing data, it may end-up in an awkward place where it is not useful for any, as it contains fields that map just to one or the other operation.

One alternative solution is to break the “Database Ecto schema Forms / API” mapping in two parts. The first will cast and validate the external data with its own structure which you then transform and write to the database. For such, let’s define a schema named Registration that will take care of casting and validating the form data exclusively, mapping directly to the UI fields:

defmodule Registration do use Ecto.Schema embedded_schema do field :first_name field :last_name field :email end end

We used embedded_schema because it is not our intent to persist it anywhere. With the schema in hand, we can use Ecto changesets and validations to process the data:

fields = [:first_name, :last_name, :email] changeset = %Registration{} |> Ecto.Changeset.cast(params["sign_up"], fields) |> validate_required(...) |> validate_length(...)

Now that the registration changes are mapped and validated, we can check if the resulting changeset is valid and act accordingly:

if changeset.valid? do # Get the modified registration struct out of the changeset registration = Ecto.Changeset.apply_changes(changeset) MyApp.Repo.transaction fn -> MyApp.Repo.insert_all "accounts", Registration.to_account(registration) MyApp.Repo.insert_all "profiles", Registration.to_profile(registration) end {:ok, registration} else # Annotate the action we tried to perform so the UI shows errors changeset = %{changeset | action: :registration} {:error, changeset} end

The to_account/1 and to_profile/1 functions in Registration would receive the registration struct and split the attributes apart accordingly:

def to_account(registration) do Map.take(registration, [:email]) end def to_profile(%{first_name: first, last_name: last}) do %{name: "#{first} #{last}"} end

In the example above, by breaking apart the mapping between the database and Elixir and between Elixir and the UI, our code becomes clearer and our data-structures simpler.

Note we have used MyApp.Repo.insert_all/2 to add data to both “accounts” and “profiles” tables directly. We have chosen to bypass schemas altogether. However, there is nothing stopping you from also defining both Account and Profile schemas and changing to_account/1 and to_profile/1 to respectively return %Account{} and %Profile{} structs. Once structs are returned, they could be inserted through the usual Repo.insert/2 operation.

Similarly, we chose to define a Registration schema to use in the changeset but Ecto 2.0 also allows developers to use changesets without schemas. We can dynamically define the data and their types. Let’s rewrite the registration changeset above to bypass schemas:

data = %{} types = %{first_name: :string, last_name: :string, email: :string} changeset = {data, types} # The data+types tuple is equivalent to %Registration{} |> Ecto.Changeset.cast(params["sign_up"], Map.keys(types)) |> validate_required(...) |> validate_length(...)

You can use this technique to validate API endpoints, search forms, and other sources of data. The choice of using schemas depends mostly if you want to use the same mapping in different places and/or if you desire the compile-time guarantees Elixir structs gives you. Otherwise, you can bypass schemas altogether, be it when using changesets or interacting with the repository.

Summary

Ecto 2.0 introduces insert_all that directly inserts data into a given table. insert_all , alongside all , update_all and delete_all , allows developers to work closer to the database without the need for using schemas.

Such possibilities make Ecto 2.0 a substantial departure from earlier versions, as developers can focus more on how their data map to different domains, like the database and the UI, relying on Ecto as a tool to interact with each of those domains in the best possible way.

This article was extracted from an ebook we’re writing we’ve written on What’s new in Ecto 2.0. Click here to receive a copy of the ebook now when it’s ready .





