Resource
Ash resources are used to model data and define actions which are used to manipulate that data. In the Ash world we often compare the resources with nouns and the actions with verbs.
To-Do-List Example
To dive into resources we use a simple to-do-list application as an example. As a preperation for this please use the Minimal Ash 2.x Setup Guide to generate a new Elixir application.
We want to create a task
resource which has a content
attribute
and an id
attribute as a primary key. We also want to include the
actions create
, read
, update
and delete
. Ash provides those
actions for free but we have to include them into the resource.
In the Ash world a resource needs to be registered to an
interal API. Please don’t think of this as an external WebAPI. Ash
uses the term API for an internal programming interface. We call
our API ToDoList .
|
Do you like video tutorials? Have a look at the 3 minute video "Ash Resource - Create, Read, Update and Destroy" in our @elixir-phoenix-ash YouTube Channel. |
Configure the internal API
We have to add the new internal API to the :ash_apis
in our
config.exs
file.
import Config
config :ash, :use_all_identities_in_manage_relationship?, false
# Add this line
config :app, :ash_apis, [App.ToDoList]
Now we create the ToDoList
API module which contains the
resource Task
.
defmodule App.ToDoList do
use Ash.Api
resources do
resource App.ToDoList.Task
end
end
For the resource(s) we create a new directory:
$ mkdir -p lib/app/to_do_list/resources
Configure the Resource
The resource defines attributes
which are the fields of the resource.
In our case we have two attributes: id
and content
. The id
attribute
is a special attribute because it is the primary key of the resource. We
use the uuid_primary_key
macro to define the id
attribute as a primary
key. The content
attribute is a simple string.
defmodule App.ToDoList.Task do
use Ash.Resource, data_layer: Ash.DataLayer.Ets (1)
attributes do
uuid_primary_key :id
attribute :content, :string
end
end
1 | In this example we use the
Ash.DataLayer.Ets as
a database layer. ETS (Erlang Term Storage) is an in memory data store
which is build into your Erlang system. For our training purpose this
is ideal because we don’t have to install and configure a database (e.g.
PostgreSQL). But ETS does not save any data to disk! With every
restart of iex you have to re-create the example data. |
The resulting directory structure should look like this:
$ tree lib
lib
├── app
│ ├── application.ex
│ ├── to_do_list
│ │ └── resources
│ │ └── task.ex
│ └── to_do_list.ex
└── app.ex
4 directories, 4 files
We now have a resource but because we haven’t defined any actions we
can’t do anything with it. Let’s add a create
action.
Create
To create a resource we have to add the needed create
action to the
resource. In addition we add a code_interface
section to the task
resource for some Ash magic which creates a
App.ToDoList.Task.create/1
and a App.ToDoList.Task.create!/1
function.
defmodule App.ToDoList.Task do
use Ash.Resource, data_layer: Ash.DataLayer.Ets
attributes do
uuid_primary_key :id
attribute :content, :string
end
actions do
defaults [:create]
end
code_interface do
define_for App.ToDoList
define :create
end
end
Fire up the IEx (Elixir’s Interactive Shell) to create your first task:
$ iex -S mix
Compiling 2 files (.ex)
Erlang/OTP 26 [erts-14.0.2] [...]
Interactive Elixir (1.15.5) [...]
iex(1)> App.ToDoList.Task.create!(%{content: "Mow the lawn"})
#App.ToDoList.Task<
__meta__: #Ecto.Schema.Metadata<:built, "">,
id: "8e868c09-c0d0-4362-8270-09272acab769",
content: "Mow the lawn",
aggregates: %{},
calculations: %{},
...
>
iex(2)>
The function App.ToDoList.Task.create!/1
raises an error if
something goes wrong (e.g. a validation error). Alternatively you can
use App.ToDoList.Task.create/1
which returns a tuple with the status
and the resource.
iex(2)> App.ToDoList.Task.create(%{content: "Mow the lawn"})
{:ok,
#App.ToDoList.Task<
__meta__: #Ecto.Schema.Metadata<:built, "">,
id: "a8430505-ef7e-4f64-bc2c-2a6db216d8ea",
content: "Mow the lawn",
aggregates: %{},
calculations: %{},
...
>}
iex(3)>
You can still create a task the long way with the following code:
|
Read
Writing is one thing but it only makes sense if you can read the written
data too. To make our life a bit easier we add a read
action and a
code_interface
define for read
:
defmodule App.ToDoList.Task do
use Ash.Resource, data_layer: Ash.DataLayer.Ets
attributes do
uuid_primary_key :id
attribute :content, :string
end
actions do
# add :read here
defaults [:create, :read]
end
code_interface do
define_for App.ToDoList
define :create
# add this line
define :read
end
end
Index
To fetch a list of all tasks in the database we can use the
App.ToDoList.Task.read!/1
(results in a list) or
App.ToDoList.Task.read/1
(results in a tuple with a status and a
list) functions. Those are automatically generated by Ash by the
code_interface
part of the task
resource.
$ iex -S mix
Compiling 2 files (.ex)
Erlang/OTP 26 [...]
Interactive Elixir (1.15.5) [...]
iex(1)> App.ToDoList.Task.create!(%{content: "Mow the lawn"})
#App.ToDoList.Task<
__meta__: #Ecto.Schema.Metadata<:loaded>,
id: "881c6c08-223c-41b1-9d61-2d3a40e478bd",
content: "Mow the lawn",
...
>
iex(2)> App.ToDoList.Task.create!(%{content: "Buy milk"})
#App.ToDoList.Task<
__meta__: #Ecto.Schema.Metadata<:loaded>,
id: "22b11587-20fe-40d2-830e-50f8930c13c9",
content: "Buy milk",
...
>
iex(3)> App.ToDoList.Task.read! |> Enum.map(& &1.content)
["Buy milk", "Mow the lawn"]
iex(4)> App.ToDoList.Task.read
{:ok,
[
#App.ToDoList.Task<
__meta__: #Ecto.Schema.Metadata<:loaded>,
id: "22b11587-20fe-40d2-830e-50f8930c13c9",
content: "Buy milk",
...
>,
#App.ToDoList.Task<
__meta__: #Ecto.Schema.Metadata<:loaded>,
id: "881c6c08-223c-41b1-9d61-2d3a40e478bd",
content: "Mow the lawn",
...
>
]}
iex(5)>
If you have an empty database this is your result for both functions:
$ iex -S mix
Erlang/OTP 26 [...]
Interactive Elixir (1.15.5) [...]
iex(1)> App.ToDoList.Task.read!
[]
iex(2)> App.ToDoList.Task.read
{:ok, []}
iex(3)>
Show
Often one wants to fetch a specific set of data by an id
. The Ash
code_interface
has an easy solution for this common scenario.
defmodule App.ToDoList.Task do
use Ash.Resource, data_layer: Ash.DataLayer.Ets
attributes do
uuid_primary_key :id
attribute :content, :string
end
actions do
defaults [:create, :read]
end
code_interface do
define_for App.ToDoList
define :create
define :read
# add this line
define :by_id, get_by: [:id], action: :read (1)
end
end
1 | This generates the functions App.ToDoList.Task.by_id/1 and
App.ToDoList.Task.by_id!/1 |
Let’s try it out:
$ iex -S mix
Erlang/OTP 26 [...]
Interactive Elixir (1.15.5) [...]
iex(1)> alias App.ToDoList.Task
App.ToDoList.Task
iex(2)> Task.read (1)
{:ok, []}
iex(3)> {:ok, task} = Task.create(%{content: "Mow the lawn"})
{:ok,
#App.ToDoList.Task<
__meta__: #Ecto.Schema.Metadata<:loaded>,
id: "a5648b48-4eb3-443d-aba7-fafbbfedc564",
content: "Mow the lawn",
...
>}
iex(4)> task.id
"a5648b48-4eb3-443d-aba7-fafbbfedc564"
iex(5)> Task.by_id("a5648b48-4eb3-443d-aba7-fafbbfedc564")
{:ok,
#App.ToDoList.Task<
__meta__: #Ecto.Schema.Metadata<:loaded>,
id: "a5648b48-4eb3-443d-aba7-fafbbfedc564",
content: "Mow the lawn",
...
>}
iex(6)> Task.by_id!("a5648b48-4eb3-443d-aba7-fafbbfedc564")
#App.ToDoList.Task<
__meta__: #Ecto.Schema.Metadata<:loaded>,
id: "a5648b48-4eb3-443d-aba7-fafbbfedc564",
content: "Mow the lawn",
...
>
1 | Just to establish that there are no tasks in the database. |
And here an example when there is no task in the database for the
given id
:
$ iex -S mix
Erlang/OTP 26 [...]
Interactive Elixir (1.15.5) [...]
iex(1)> App.ToDoList.Task.by_id("not-in-the-db")
{:error,
%Ash.Error.Query.NotFound{
primary_key: nil,
resource: App.ToDoList.Task,
changeset: nil,
query: nil,
error_context: [],
vars: [],
path: [],
stacktrace: #Stacktrace<>,
class: :invalid
}}
iex(2)> App.ToDoList.Task.by_id!("not-in-the-db")
** (Ash.Error.Query.NotFound) record not found
[...]
Update
Ash provides a simple way to update a resource and by now you can probably guess how it works:
defmodule App.ToDoList.Task do
use Ash.Resource, data_layer: Ash.DataLayer.Ets
attributes do
uuid_primary_key :id
attribute :content, :string
end
actions do
# add :update to the list
defaults [:create, :read, :update]
end
code_interface do
define_for App.ToDoList
define :create
define :read
define :by_id, get_by: [:id], action: :read
# add this line
define :update
end
end
Let’s try it out:
$ iex -S mix
Erlang/OTP 26 [...]
Interactive Elixir (1.15.5) [...]
iex(1)> alias App.ToDoList.Task
App.ToDoList.Task
iex(2)> {:ok, task} = Task.create(%{content: "Mow the lawn"})
{:ok,
#App.ToDoList.Task<
__meta__: #Ecto.Schema.Metadata<:loaded>,
id: "d4c8cb9a-10b7-45f4-bece-dcea0fd16e5f",
content: "Mow the lawn",
...
>}
iex(3)> Task.update(task, %{content: "Play golf"})
{:ok,
#App.ToDoList.Task<
__meta__: #Ecto.Schema.Metadata<:loaded>,
id: "d4c8cb9a-10b7-45f4-bece-dcea0fd16e5f",
content: "Play golf",
...
>}
iex(4)> Task.update!(task, %{content: "Buy milk"})
#App.ToDoList.Task<
__meta__: #Ecto.Schema.Metadata<:loaded>,
id: "d4c8cb9a-10b7-45f4-bece-dcea0fd16e5f",
content: "Buy milk",
...
>
Destroy (delete)
And finally we can destroy a resource. Again, this is very similar to the other actions:
defmodule App.ToDoList.Task do
use Ash.Resource, data_layer: Ash.DataLayer.Ets
attributes do
uuid_primary_key :id
attribute :content, :string
end
actions do
# add :delete to list
defaults [:create, :read, :update, :destroy]
end
code_interface do
define_for App.ToDoList
define :create
define :read
define :by_id, get_by: [:id], action: :read
define :update
# Add this line
define :destroy
end
end
Let’s try it out:
iex -S mix
Erlang/OTP 26 [...]
Interactive Elixir (1.15.5) [...]
iex(1)> {:ok, task} = App.ToDoList.Task.create(%{content: "Mow the lawn"})
{:ok,
#App.ToDoList.Task<
__meta__: #Ecto.Schema.Metadata<:loaded>,
id: "5bd2b15e-fd29-4d3f-9356-cbfe06ea7eee",
content: "Mow the lawn",
...
>}
iex(2)> App.ToDoList.Task.destroy(task)
:ok
iex(3)> App.ToDoList.Task.by_id(task.id) (1)
{:error,
%Ash.Error.Query.NotFound{
primary_key: nil,
resource: App.ToDoList.Task,
changeset: nil,
query: nil,
error_context: [],
vars: [],
path: [],
stacktrace: #Stacktrace<>,
class: :invalid
}}
iex(4)>
1 | Because the task is destroyed we can’t find it anymore. |