DEV Community

Lubien
Lubien

Posted on • Updated on

The Lazy Programmer's Intro to LiveView: Chapter 4

Go to Chapter 1

Adding points to users

In the previous chapter, we just added the points column to users table. We need a way to add points to users. This time we are going to learn how to use Ecto to modify rows in our database.

Ecto Changesets 101

Ecto comes with a powerful abstraction to validate and map data from user input to the database, it's called Ecto.Changeset. Think of it as an intermediate state between user input and actually storing the data.

Words "Model+User input" with a arrow pointing to "Changet" with another arrow pointing to "Stored in Database"

User inputs usually come in the form of an Elixir map (a key-value structure) such as %{"points" => 10}. Since we want to make changes, we must change something. If you wanted to update an existing user, you first need to get that user then you'd have a variable with a value such as %User{email: "…", …}. Combine both with a change_* function and we have a changeset. Let's do it from scratch!

Since we are going to make a function that modifies our users it makes sense to use the accounts.ex context and since this is related to our Ecto model we will also be modifying our user.ex file too. With the same reasoning that means we will be testing this at accounts_test.exs.

Head out to lib/champions/accounts/user.ex. You'll notice there are multiple *_changeset named functions with specific intents. Scroll to the bottom and create a new function called points_changeset/2.

@doc """
A user changeset to update points
"""
def points_changeset(user, attrs) do
  user
  |> cast(attrs, [:points])
  |> validate_required([:points])
  |> validate_number(:points, greater_than_or_equal_to: 0)
end
Enter fullscreen mode Exit fullscreen mode

There's a lot to unpack so let's go bit by bit. We use @doc to give this function a nice documentation for the developers of the future who want to use this. Following that we define a function that takes two arguments: the user to be modified and the new data on the attrs variable.

The fun comes with the pipes. It's all about transformation. On line 5 we start with the user as-is. On line 6 we use cast/4 to convert the user into a changeset but we limit the attrs to only read points changes and ignore everything else. Next, we do two validations: we want to make points mandatory and it should be a non-negative integer. By the end of this pipe chain, we have a validated changeset without any trouble at all.

Then, we need to expose this function to our Accounts context. Head out to lib/champions/accounts.ex and create a new function in the end called change_user_points/2.

@doc """
Returns an `%Ecto.Changeset{}` for tracking user changes.

## Examples

    iex> change_user_points(user)
    %Ecto.Changeset{data: %User{}}

"""
def change_user_points(%User{} = user, attrs \\ %{}) do
  User.points_changeset(user, attrs)
end
Enter fullscreen mode Exit fullscreen mode

As you can see this is just an alias to the changeset contained in the Ecto model. Making your context module the place to talk to the external world (your_app_web) is a good practice. Last but not least, we need to test that this actually works. go to test/champions/accounts_test.exs and add a new test case in the end:

describe "change_user_points/2" do
  test "accepts non-negative integers" do
    assert %Ecto.Changeset{} = changeset = Accounts.change_user_points(%User{}, %{"points" => -1})
    refute changeset.valid?

    assert %Ecto.Changeset{} = changeset = Accounts.change_user_points(%User{}, %{"points" => 0})
    assert changeset.valid?

    assert %Ecto.Changeset{} = changeset = Accounts.change_user_points(%User{}, %{"points" => 10})
    assert changeset.valid?
  end
end
Enter fullscreen mode Exit fullscreen mode

With this test, we pass an empty user and a negative, zero, and positive amount of points. Note that at all times it returns an Ecto.Changeset, what changes is that if it's invalid the changeset valid? property will be false.

Applying changesets using Repo

Whenever an Ecto user wants to apply changes to the database they must go through a Repo. Think of those as abstractions between Elixir and the database itself. You don't need to handle creating connections, recovering from disconnections, writing SQL, and many other things because Repo will take care of that for you alongside your changeset. Since Phoenix by default comes with Postgres support you should have a Champions.Repo module ready to be used at any time, otherwise not even the migrations would have worked.

Let's create a high-level function on Accounts that takes a user and a number of points and updated the user points. Inside accounts.ex create a new function update_user_points/2:

@doc """
Updates the current number of points of a user

## Examples

    iex> update_user_points(user, 10)
    {:ok, %User{points: 10}}

"""
def update_user_points(%User{} = user, points) do
  user
  |> change_user_points(%{"points" => points})
  |> Repo.update()
end
Enter fullscreen mode Exit fullscreen mode

The only new thing here is that there's a pipe after the changeset that calls Repo.update/2 to trigger the changes to the database. In summary, the changeset works as a mapping of what needs to be done in the database and Repo actually applies those changes. To test this out add a new unit test to accounts_test.exs:

describe "set_user_points/2" do
  setup do
    %{user: user_fixture()}
  end

  test "updates the amounts of points of an existing user", %{user: user} do
    {:ok, updated_user} = Accounts.update_user_points(user, 10)
    assert updated_user.points == 10
  end
end
Enter fullscreen mode Exit fullscreen mode

A cool thing worth mentioning here is that out is that the Phoenix auth generator also created the helper user_fixture/1 function that creates a user with zero trouble so your unit tests can be DRY.

Summary

  • To store data in the database Ecto uses changesets.
  • Changesets validate and prepare which data can go to the database, you can even have more than one per Ecto model. Ecto.Changeset comes with useful validation functions.
  • Repo is how Ecto talks to the database.
  • Repo.update/2 reads a changeset and applies changes to the database.

Read chapter 5: Listing our users

Top comments (0)