CRUDy contexts can jam up the works in an Elixir/Phoenix application.
Contexts allow for a separation of concerns between data and business logic. Often times, the first thing we do is add many basic CRUD functions to our contexts which essentially delegate to Ecto. This boilerplate code leads to boilerplate tests.
Over time, these CRUD operations add up in the application and compound the problem. The names and APIs of these standard functions can start to drift between modules, causing unexpected behavior and bugs. In short, it can be a mess.
EctoResource cleans up the mess
EctoResource is a Hex package that aims to reduce the boilerplate code involved in basic CRUD operations. It does so by providing macros to generate all the CRUD functions for a given Ecto Repo and Schema.
Consider the following context without using ecto_resource (generated by mix phx.gen.context Accounts User users
):
defmodule MyApp.Accounts do
@moduledoc """
The Accounts context.
"""
import Ecto.Query, warn: false
alias MyApp.Accounts.User
alias MyApp.Repo
def list_users do
Repo.all(User)
end
def get_users!(id), do: Repo.get!(User, id)
def create_users(attrs \\ %{}) do
%User{}
|> User.changeset(attrs)
|> Repo.insert()
end
def update_users(%User{} = users, attrs) do
users
|> User.changeset(attrs)
|> Repo.update()
end
def delete_users(%User{} = users) do
Repo.delete(users)
end
def change_users(%User{} = users) do
User.changeset(users, %{})
end
end
All of the functions here are mostly delegates to Repo
, with the addition of binding the operations to the User
module.
Alternatively, we can generate these functions using EctoResource
. This alleviates the boilerplate and allows the functions within the module to be focused on more sophisticated operations.
defmodule MyApp.Accounts do
@moduledoc """
The Accounts context.
"""
import Ecto.Query, warn: false
alias MyApp.Accounts.User
alias MyApp.Repo
use EctoResource
using_repo(Repo) do
resource(User)
end
end
By default resource
will generate all the basic CRUD functions:
MyApp.Accounts.all_users/1
MyApp.Accounts.change_user/1
MyApp.Accounts.create_user/1
MyApp.Accounts.create_user!/1
MyApp.Accounts.delete_user/1
MyApp.Accounts.delete_user!/1
MyApp.Accounts.get_user/2
MyApp.Accounts.get_user!/2
MyApp.Accounts.get_user_by/2
MyApp.Accounts.get_user_by!/2
MyApp.Accounts.update_user/2
MyApp.Accounts.update_user!/2
Note: You can generate this list yourself by using the following function in the console: iex> {repo, schema, function_names} = MyContext.__resource__(:resources)
If you need to be more specific you can use the only
filter option:
...
using_repo(Repo) do
resource(User, only: [:create, :delete])
end
...
Which would generate only the given functions
MyApp.Accounts.create_user/1
MyApp.Accounts.delete_user/1
You can also use the except
filter option, which is the inverse of only
:
...
using_repo(Repo) do
resource(User, except: [:create, :create!, :delete, :delete!])
end
...
Which would generate all but the given functions
MyApp.Accounts.all_users/1
MyApp.Accounts.change_user/1
MyApp.Accounts.get_user/2
MyApp.Accounts.get_user!/2
MyApp.Accounts.get_user_by/2
MyApp.Accounts.get_user_by!/2
MyApp.Accounts.update_user/2
MyApp.Accounts.update_user!/2
You can see the EctoResource
macros eliminate boilerplate function definitions. This lets our context modules remain focused on those functions that deviate from the standard CRUD operations. The functions generated by EctoResource
all have the same API, providing a dependable standard.
You might be delighted to know that all the functions generated in your contexts will be included in your application’s documentation if you’re using Exdoc.
If you’d like to see how much can be removed from your contexts you can install EctoResource
in your application by adding it to your dependencies:
...
defp deps do
[
...
{:ecto_resource, "~> 1.2"},
...
]
end
...
After a mix deps.get
, you’ll be all set and ready to use the EctoResource
macros in your application! Check it out on Hex. Read the documentation.