PostgreSQL Data Layer Minimal Setup
We start with a fresh project and give the new application the name app
:
$ mix new --sup app
$ cd app
Please go to PostgreSQL Data Layer Ash + Phoenix Framework Setup if you are looking for an Ash + Phoenix Framework setup. |
Add Ash and AshPostgres as a dependency:
[...]
defp deps do
[
{:ash, "~> 2.15.8"}, (1)
{:ash_postgres, "~> 1.3.52"} (2)
]
end
[...]
1 | Find the latest version number at https://hex.pm/packages/ash |
2 | Find the latest version number at https://hex.pm/packages/ash_postgres |
Run mix deps.get
to install it:
$ mix deps.get
To make sure that we get the right formatting for our code, we change the file .formatter.exs
to include the Ash and AshPostgres formatter:
[
import_deps: [:ash, :ash_postgres], (1)
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
]
1 | Add this line to your .formatter.exs file. |
Next we put the AshPostgres.Repo
wrapper around the Ecto.Repo
:
defmodule App.Repo do
use AshPostgres.Repo, otp_app: :app
end
We have to create a couple of general config files for the different environments:
import Config
config :app,
ash_apis: [App.Shop] (1)
config :app,
ecto_repos: [App.Repo]
import_config "#{config_env()}.exs" (2)
1 | Please adapt App.Shop to your application. Since we use App.Shop in most of our examples I use it here as well. |
2 | This imports the config file for the current environment. |
import Config
config :app, App.Repo,
username: "postgres", (1)
password: "postgres",
hostname: "localhost",
database: "shop_dev", (2)
port: 5432,
show_sensitive_data_on_connection_error: true,
pool_size: 10
1 | Please use the correct username and password for your database. Most developers use postgres:postgres for the development system. |
2 | Please use the database name which makes most sense for you. |
import Config
if config_env() == :prod do
database_url =
System.get_env("DATABASE_URL") || (1)
raise """
environment variable DATABASE_URL is missing.
For example: ecto://USER:PASS@HOST/DATABASE
"""
config :app, App.Repo,
url: database_url,
pool_size: String.to_integer(System.get_env("POOL_SIZE") || "10")
end
1 | Most production environments use environment variables to configure the database connection. This is the default for Heroku and other cloud providers. If you don’t use environment variables, you can hardcode the database connection here. But in that case take extra security messures to protect your database credentials. |
import Config
# Run `mix help test` for more information.
config :app, App.Repo,
username: "postgres",
password: "postgres",
hostname: "localhost",
database: "app_test#{System.get_env("MIX_TEST_PARTITION")}",
pool: Ecto.Adapters.SQL.Sandbox,
pool_size: 10
defmodule App.Application do
# See https://hexdocs.pm/elixir/Application.html
# for more information on OTP Applications
@moduledoc false
use Application
@impl true
def start(_type, _args) do
children = [
# Starts a worker by calling: App.Worker.start_link(arg)
# {App.Worker, arg}
App.Repo (1)
]
# See https://hexdocs.pm/elixir/Supervisor.html
# for other strategies and supported options
opts = [strategy: :one_for_one, name: App.Supervisor]
Supervisor.start_link(children, opts)
end
end
1 | Add this line. |
To create a new database the app has to have at least one resource. The creation of a resource is the next step.
Add AshPostgres to a Resource
As an example we add a minimal Product
resource to our application. The resource is more or less empty. We add more attributes later.
defmodule App.Shop.Product do
use Ash.Resource,
data_layer: AshPostgres.DataLayer (1)
postgres do
table "products" (2)
repo App.Repo
end
attributes do
uuid_primary_key :id (3)
end
actions do
defaults [:create, :read, :update, :destroy]
end
code_interface do
define_for App.Shop
define :create
define :read
define :by_id, get_by: [:id], action: :read
define :update
define :destroy
end
end
1 | Tells Ash to use the AshPostgres.DataLayer for this resource. |
2 | Sets the name of the table in the database. |
3 | An AshPostgres resource always has to have at least one UUID primary key attribute. |
Of course we need to add an internal API:
defmodule App.Shop do
use Ash.Api
resources do
resource App.Shop.Product
end
end
The products
table is not yet created. Not even the database is created. We do that in the next step.
Create the Database
Assuming that you have PostgreSQL installed and running on your system, you can now create the database with the mix ash_postgres.create
command:
$ mix ash_postgres.create
Compiling 2 files (.ex)
Generated app app
The database for App.Repo has been created
Drop the Database
In case you need to drop (delete) the database you can use the mix ash_postgres.drop
command:
$ mix ash_postgres.drop
The database for App.Repo has been dropped
Please re-run mix ash_postgres.create now in case you ran the drop command by accident while working this tutorial. |
mix ash.codegen
mix ash.codegen
scans your application for resources, keeps track of them and generates migrations if things (e.g. attributes) change.
We created the database but it is still empty. It is time to use mix ash.codegen
to create a migration for the Product
resource.
$ mix ash.codegen
Running codegen for AshPostgres.DataLayer...
Compiling 1 file (.ex)
Extension Migrations:
No extensions to install
Generating Tenant Migrations:
Generating Migrations:
* creating priv/repo/migrations/20231005153554_migrate_resources1.exs
It is not a bad habit to check the generated migration file before running the migration. In our case it looks like this:
[...]
def up do
create table(:products, primary_key: false) do (1)
add :id, :uuid, null: false, primary_key: true (2)
end
end
def down do
drop table(:products)
end
[...]
1 | Create a table named products . |
2 | Add a primary key column named id of type uuid . |
mix ash_postgres.migrate
Now it is time to run the migration:
$ mix ash_postgres.migrate
17:08:26.221 [info] == Running 20231005150754 App.Repo.Migrations.MigrateResources1.up/0 forward
17:08:26.222 [info] create table products
17:08:26.226 [info] == Migrated 20231005150754 in 0.0s
$
If you want to you can check the table with psql
:
$ psql -h localhost -U postgres -d shop_dev -c "\d products"
Table "public.products"
Column | Type | Collation | Nullable | Default
--------+------+-----------+----------+---------
id | uuid | | not null |
Indexes:
"products_pkey" PRIMARY KEY, btree (id)
mix ash_postgres.rollback
Sometimes you want to undo a migration. You can do that with mix ash_postgres.rollback
:
$ mix ash_postgres.rollback
14:29:49.017 [info] == Running 20231006105336 App.Repo.Migrations.MigrateResources5.down/0 forward
14:29:49.018 [info] drop table products
14:29:49.019 [info] == Migrated 20231006105336 in 0.0s
In case you just did a rollback in this example you want to migrate again with mix ash_postgres.migrate before you continue. |
Add Attributes to a Resource
Let’s add two attributes to the Product
resource:
defmodule App.Shop.Product do
use Ash.Resource,
data_layer: AshPostgres.DataLayer
postgres do
table "products"
repo App.Repo
end
attributes do
uuid_primary_key :id
attribute :name, :string (1)
attribute :price, :decimal (2)
end
actions do
defaults [:create, :read, :update, :destroy]
end
code_interface do
define_for App.Shop
define :create
define :read
define :by_id, get_by: [:id], action: :read
define :by_name, get_by: [:name], action: :read (3)
define :update
define :destroy
end
end
1 | A :name attribute of type :string. |
2 | A :price attribute of type :decimal. |
3 | A :by_name action that can be used to find a product by its name. |
Start mix ash.codegen
again:
$ mix ash.codegen
Running codegen for AshPostgres.DataLayer...
Compiling 1 file (.ex)
Extension Migrations:
No extensions to install
Generating Tenant Migrations:
Generating Migrations:
* creating priv/repo/migrations/20231005155818_migrate_resources2.exs (1)
$ mix ash_postgres.migrate (2)
17:58:36.046 [info] == Running 20231005155818 App.Repo.Migrations.MigrateResources2.up/0 forward
17:58:36.047 [info] alter table products
17:58:36.050 [info] == Migrated 20231005155818 in 0.0s
$
1 | mix ash.codegen created a new migration file which includes the new attributes. |
2 | mix ash_postgres.migrate runs the migration. |
Because we are curious we check the table again:
$ psql -h localhost -U postgres -d shop_dev -c "\d products"
Table "public.products"
Column | Type | Collation | Nullable | Default
--------+---------+-----------+----------+---------
id | uuid | | not null |
name | text | | |
price | numeric | | |
Indexes:
"products_pkey" PRIMARY KEY, btree (id)
Time to add two entries into the products table (a Banana and a Pineapple):
$ iex -S mix
iex(1)> alias App.Shop.Product
App.Shop.Product
iex(2)> Product.create!(%{name: "Banana", price: 0.10})
18:04:58.761 [debug] QUERY OK db=12.5ms idle=795.4ms
begin []
18:04:58.777 [debug] QUERY OK db=3.9ms
INSERT INTO "products" ("id","name","price") VALUES ($1,$2,$3)
RETURNING "price","name","id" ["4d7e383b-ce7b-44d0-818c-290eaa8b0532",
"Banana", Decimal.new("0.1")] (1)
18:04:58.781 [debug] QUERY OK db=0.8ms
commit []
#App.Shop.Product<
__meta__: #Ecto.Schema.Metadata<:loaded, "products">,
id: "4d7e383b-ce7b-44d0-818c-290eaa8b0532",
name: "Banana",
price: Decimal.new("0.1"),
...
>
iex(3)> Product.create!(%{name: "Pineapple", price: 0.50})
18:05:24.885 [debug] QUERY OK db=0.3ms idle=935.2ms
begin []
18:05:24.887 [debug] QUERY OK db=0.6ms
INSERT INTO "products" ("id","name","price") VALUES ($1,$2,$3)
RETURNING "price","name","id" ["e854a911-4cda-4693-bd49-db200b675ded",
"Pineapple", Decimal.new("0.5")]
18:05:24.888 [debug] QUERY OK db=0.6ms
commit []
#App.Shop.Product<
__meta__: #Ecto.Schema.Metadata<:loaded, "products">,
id: "e854a911-4cda-4693-bd49-db200b675ded",
name: "Pineapple",
price: Decimal.new("0.5"),
...
>
iex(4)>
1 | In development mode you see these SQL debugging messages. |
After pressing Ctrl-C
two times to exit the iex
session we can check the table again:
$ psql -h localhost -U postgres -d shop_dev -c "select * from products"
id | name | price
--------------------------------------+-----------+-------
4d7e383b-ce7b-44d0-818c-290eaa8b0532 | Banana | 0.1
e854a911-4cda-4693-bd49-db200b675ded | Pineapple | 0.5
(2 rows)
Congratulation! You just created your first Ash application with a PostgreSQL database.
Never forget to run mix ash.codegen and mix ash_postgres.migrate after you change a resource. Otherwise the changes will not be reflected in the database. |