Check out the Alchemist's Edition

#46: Ecto Virtual Attributes

Elixir 1.5

Phoenix 1.3


In this episode let’s see how we can use virtual attributes in Ecto to solve a common problem - when we need to include fields that don’t map to columns in the database.

Here we have a signup form with email, full name, and password fields

And now instead of displaying a full name field, we want to break it up into two field - one for the first name and one for the last name.

Let’s open our signup template and change our form to use a first name and last name.

Template path: lib/teacher_web/templates/user/form.html.eex

...
<div class="form-group">
  <%= label f, :first_name, class: "control-label" %>
  <%= text_input f, :first_name, class: "form-control" %>
  <%= error_tag f, :first_name %>
</div>

<div class="form-group">
  <%= label f, :last_name, class: "control-label" %>
  <%= text_input f, :last_name, class: "form-control" %>
  <%= error_tag f, :last_name %>
</div>
...

Then we’ll go to our user.ex module and add the first name and last name as virtual attributes.

lib/teacher/accounts/user.ex

...
schema "users" do
  field :encrypted_password, :string
  field :email, :string
  field :full_name, :string
  field :first_name, :string, virtual: true
  field :last_name, :string, virtual: true

  timestamps()
end
...

Now let’s go to our browser and if we try to signup we get an error.

This is because we’re now collecting the first and last name and submitting them to the server, but we’re not using them to build a full name - which as we can see in our changeset function is a required field. Let’s fix that.

We’ll need to create a function that will get the first and last name from the changeset and combine them to create our full name that we can then persist to the database. Let’s define a private function named build_full_name that will take the changeset as a parameter.

Then we can use the Ecto.Changeset.get_field function to grab the first name and then the last name. Then we’ll use Ecto.Changeset.put_change to update our changes with the full_name key.

And for the value we’ll use string interpolation to build our name. put_change will return our updated changeset, which is great because we can now include our new build_full_name function in our existing pipeline.

So let’s go back to our changeset function and include build_full_name. We’ll also need to update our cast function to include :first_name and :last_name.

lib/teacher/accounts/user.ex

...
def changeset(%User{} = user, attrs) do
  user
  |> cast(attrs, [:email, :encrypted_password, :first_name, :last_name])
  |> build_full_name()
  |> validate_required([:email, :encrypted_password, :full_name])
  |> unique_constraint(:email)
  |> update_change(:encrypted_password, &Bcrypt.hashpwsalt/1)
end

defp build_full_name(changeset) do
  first_name = get_field(changeset, :first_name)
  last_name = get_field(changeset, :last_name)
  put_change(changeset, :full_name, "#{first_name} #{last_name}")
end

And if we go back to the browser and sign up - it works.

Let’s also check the database just to make sure. And our new user is in there with the full name in the proper format.