EctoResource (ecto_resource v1.4.0)

EctoResource

Eliminate boilerplate involved in defining basic CRUD functions in a Phoenix context or Elixir module.

When using Context modules in a Phoenix application, there's a general need to define the standard CRUD functions for a given Ecto.Schema. Phoenix context generators will even do this automatically. Soon you will notice that there's quite a lot of code involved in CRUD access within your contexts.

This can become problematic for a few reasons:

  • Boilerplate functions for CRUD access, for every Ecto.Schema referenced in that context, introduce more noise than signal. This can obscure the more interesting details of the context.
  • These functions may tend to accumulate drift from the standard API by inviting edits for new use-cases, reducing the usefulness of naming conventions.
  • The burden of locally testing wrapper functions, yields low value for the writing and maintainence investment.

In short, at best this code is redundant and at worst is a deviant entanglement of modified conventions. All of which amounts to a more-painful development experience. EctoResource was created to ease this pain.

Usage

Basic usage - generate all EctoResource functions

defmodule MyApp.MyContext do
alias MyApp.Repo
alias MyApp.Schema
use EctoResource

using_repo(Repo) do
  resource(Schema)
end
end

This generates all the functions EctoResource has to offer:

  • MyContext.all_schemas/1
  • MyContext.change_schema/1
  • MyContext.create_schema/1
  • MyContext.create_schema!/1
  • MyContext.delete_schema/1
  • MyContext.delete_schema!/1
  • MyContext.get_schema/2
  • MyContext.get_schema!/2
  • MyContext.get_schema_by/2
  • MyContext.get_schema_by!/2
  • MyContext.update_schema/2
  • MyContext.update_schema!/2

Explicit usage - generate only given functions

defmodule MyApp.MyContext do
alias MyApp.Repo
alias MyApp.Schema
use EctoResource

using_repo(Repo) do
  resource(Schema, only: [:create, :delete!])
end
end

This generates only the given functions:

  • MyContext.create_schema/1
  • MyContext.delete_schema!/1

Exclusive usage - generate all but the given functions

defmodule MyApp.MyContext do
alias MyApp.Repo
alias MyApp.Schema
use EctoResource

using_repo(Repo) do
  resource(Schema, except: [:create, :delete!])
end
end

This generates all the functions excluding the given functions:

  • MyContext.all_schemas/1
  • MyContext.change_schema/1
  • MyContext.create_schema!/1
  • MyContext.delete_schema/1
  • MyContext.get_schema/2
  • MyContext.get_schema_by/2
  • MyContext.get_schema_by!/2
  • MyContext.get_schema!/2
  • MyContext.update_schema/2
  • MyContext.update_schema!/2

Alias :read - generate data access functions

defmodule MyApp.MyContext do
alias MyApp.Repo
alias MyApp.Schema
use EctoResource

using_repo(Repo) do
  resource(Schema, :read)
end
end

This generates all the functions necessary for reading data:

  • MyContext.all_schemas/1
  • MyContext.get_schema/2
  • MyContext.get_schema!/2

Alias :read_write - generate data access and manipulation functions, excluding delete

defmodule MyApp.MyContext do
alias MyApp.Repo
alias MyApp.Schema
use EctoResource

using_repo(Repo) do
  resource(Schema, :read_write)
end
end

This generates all the functions except delete_schema/1 and delete_schema!/1:

  • MyContext.all_schemas/1
  • MyContext.change_schema/1
  • MyContext.create_schema/1
  • MyContext.create_schema!/1
  • MyContext.get_schema/2
  • MyContext.get_schema!/2
  • MyContext.update_schema/2
  • MyContext.update_schema!/2

Resource functions

The general idea of the generated resource functions is to abstract away the Ecto.Repo and Ecto.Schema parts of data access with Ecto and provide an API to the context that feels natural and clear to the caller.

The following examples will all assume a repo named Repo and a schema named Person.

all_people

Fetches a list of all %Person{} entries from the data store. Note: EctoResource will pluralize this function name using Inflex

iex> all_people()
[%Person{id: 1}]

iex> all_people(preloads: [:address])
[%Person{id: 1, address: %Address{}}]

iex> all_people(order_by: [desc: :id])
[%Person{id: 2}, %Person{id: 1}]

iex> all_people(preloads: [:address], order_by: [desc: :id]))
[
%Person{
  id: 2,
  address: %Address{}
},
%Person{
  id: 1,
  address: %Address{}
}
]

iex> all_people(where: [id: 2])
[%Person{id: 2, address: %Address{}}]

change_person

Creates a %Person{} changeset.

iex> change_person(%{name: "Example Person"})
#Ecto.Changeset<
action: nil,
changes: %{name: "Example Person"},
errors: [],
data: #Person<>,
valid?: true
>

create_person

Inserts a %Person{} with the given attributes in the data store, returning an :ok/:error tuple.

iex> create_person(%{name: "Example Person"})
{:ok, %Person{id: 123, name: "Example Person"}}

iex> create_person(%{invalid: "invalid"})
{:error, %Ecto.Changeset}

create_person!

Inserts a %Person{} with the given attributes in the data store, returning a %Person{} or raises Ecto.InvalidChangesetError.

iex> create_person!(%{name: "Example Person"})
%Person{id: 123, name: "Example Person"}

iex> create_person!(%{invalid: "invalid"})
** (Ecto.InvalidChangesetError)

delete_person

Deletes a given %Person{} from the data store, returning an :ok/:error tuple.

iex> delete_person(%Person{id: 1})
{:ok, %Person{id: 1}}

iex> delete_person(%Person{id: 999})
{:error, %Ecto.Changeset}

delete_person!

Deletes a given %Person{} from the data store, returning the deleted %Person{}, or raises Ecto.StaleEntryError.

iex> delete_person!(%Person{id: 1})
%Person{id: 1}

iex> delete_person!(%Person{id: 999})
** (Ecto.StaleEntryError)

get_person

Fetches a single %Person{} from the data store where the primary key matches the given id, returns a %Person{} or nil.

iex> get_person(1)
%Person{id: 1}

iex> get_person(999)
nil

iex> get_person(1, preloads: [:address])
%Person{
  id: 1,
  address: %Address{}
}

get_person!

Fetches a single %Person{} from the data store where the primary key matches the given id, returns a %Person{} or raises Ecto.NoResultsError.

iex> get_person!(1)
%Person{id: 1}

iex> get_person!(999)
** (Ecto.NoResultsError)

iex> get_person!(1, preloads: [:address])
%Person{
  id: 1,
  address: %Address{}
}

get_person_by

Fetches a single %Person{} from the data store where the attributes match the given values.

iex> get_person_by(%{name: "Chuck Norris"})
%Person{name: "Chuck Norris"}

iex> get_person_by(%{name: "Doesn't Exist"})
nil

get_person_by!

Fetches a single %Person{} from the data store where the attributes match the given values. Raises an Ecto.NoResultsError if the record does not exist

iex> get_person_by!(%{name: "Chuck Norris"})
%Person{name: "Chuck Norris"}

iex> get_person_by!(%{name: "Doesn't Exist"})
** (Ecto.NoResultsError)

update_person

Updates a given %Person{} with the given attributes, returns an :ok/:error tuple.

iex> update_person(%Person{id: 1}, %{name: "New Person"})
{:ok, %Person{id: 1, name: "New Person"}}

iex> update_person(%Person{id: 1}, %{invalid: "invalid"})
{:error, %Ecto.Changeset}

update_person!

Updates a given %Person{} with the given attributes, returns a %Person{} or raises Ecto.InvalidChangesetError.

iex> update_person!(%Person{id: 1}, %{name: "New Person"})
%Person{id: 1, name: "New Person"}

iex> update_person!(%Person{id: 1}, %{invalid: "invalid"})
** (Ecto.InvalidChangesetError)

Caveats

This is not meant to be used as a wrapper for all the Repo functions within a context. Not all callbacks defined in Ecto.Repo are generated. EctoResource should be used to help reduce boilerplate code and tests for general CRUD operations.

It may be the case that EctoResource needs to evolve and provide slightly more functionality/flexibility in the future. However, the general focus is reducing boilerplate code.

Summary

Functions

Macro to define CRUD methods for the given Ecto.Repo in the using module.

Macro to define schema access within a given Ecto.Repo

Functions

Link to this macro

__using__(_)

(macro)

Macro to import EctoResource.using_repo/2

Examples

use EctoResource
Link to this macro

resource(schema, options \\ [])

(macro)

Macro to define CRUD methods for the given Ecto.Repo in the using module.

Examples

using(Repo) do
  resource(Schema)
end

using(Repo) do
  resource(Schema, suffix: false)
end

using(Repo) do
  resource(Schema, only: [:get])
end

using(Repo) do
  resource(Schema, except: [:delete])
end

using(Repo) do
  resource(Schema, :read)
end

using(Repo) do
  resource(Schema, :write)
end

using(Repo) do
  resource(Schema, :delete)
end
Link to this macro

using_repo(repo, list)

(macro)

Macro to define schema access within a given Ecto.Repo

Examples

using_repo(Repo) do
  resource(Schema)
end