本機要先有 PostgreSQL 在跑, 帳號密碼用預設的 postgres / postgres。然後兩個指令:
sh <(curl 'https://ash-hq.org/install/demo_cms?install=phoenix') \
&& cd demo_cms && mix igniter.install ash ash_phoenix \
ash_postgres ash_authentication ash_authentication_phoenix \
ash_admin live_debugger --auth-strategy magic_link --setup \
--yes
這個指令建一個 Phoenix 1.8 專案並裝 Ash 相關套件,
每個套件的 installer 會直接修改專案 (config、router、repo、formatter), 建資料庫並跑 migration
今天我們列出 Ash 提供的 mix task
並與目前正在使用的 phoenix, ecto 的 task 比較
Phoenix + Ecto: mix ecto.create、mix ecto.migrate、mix ecto.drop, 還有 alias mix ecto.setup 跟 mix ecto.reset。
Ash: mix ash.setup、mix ash.reset、mix ash.tear_down。做的事一樣, 差別是它會去問專案裡每一個 data layer「setup 對你來說是什麼」。對 AshPostgres 來說就是建資料庫、跑 migration、裝 Ash 需要的 Postgres function 跟 extension (installer 的 migration 就建了 citext)。
$ mix ash.setup
Getting extensions in current project...
Running setup for AshPostgres.DataLayer...
The database for DemoCms.Repo has already been created
[info] Migrations already up
installer 也改了專案的 alias, 所以 mix setup 跟 mix test 現在呼叫的是 ash.setup 而不是 ecto.setup:
setup: ["deps.get", "ash.setup", "assets.setup", "assets.build", "run priv/repo/seeds.exs"],
test: ["ash.setup --quiet", "test"],
Ecto 的 task 都還在。ash.* 是疊在上面的一層, 不是取代。
Phoenix + Ecto: mix phx.gen.schema 寫一個 schema module 跟一個 migration。mix phx.gen.context 寫一個 context module, 裡面是包著 schema 的函式。mix phx.gen.live 跟 mix phx.gen.html 兩個都做, 再加上 web 那層。
Ash: mix ash.gen.domain 跟 mix ash.gen.resource。domain 對應的是 context: 一群 resource 的集合。resource 對應的是 schema, 但它同時裝著原本放在 context 函式裡的東西。
mix ash.gen.resource DemoCms.Content.Post \
--uuid-primary-key id \
--attribute title:string:required:public,body:string:public \
--default-actions read,create \
--timestamps \
--extend postgres
DemoCms.Content 本來不存在, 所以順便建好, 並把 resource 註冊進去:
defmodule DemoCms.Content do
use Ash.Domain, otp_app: :demo_cms
resources do
resource DemoCms.Content.Post
end
end
resource, lib/demo_cms/content/post.ex:
defmodule DemoCms.Content.Post do
use Ash.Resource,
otp_app: :demo_cms,
domain: DemoCms.Content,
data_layer: AshPostgres.DataLayer
postgres do
table "posts"
repo DemoCms.Repo
end
actions do
defaults [:read, create: [:title, :body]]
end
attributes do
uuid_primary_key :id
attribute :title, :string do
allow_nil? false
public? true
end
attribute :body, :string do
public? true
end
timestamps()
end
end
attribute、一張表、兩個 action。每一行是什麼意思是接下來幾篇的主題, 今天知道這個檔案存在就夠了。產生器也把 domain 加進 config :demo_cms, ash_domains: [DemoCms.Content, DemoCms.Accounts], Ash 的工具就是靠這個清單找 domain, 不會去掃 lib/。
另外兩組產生器, 之後會用到:
mix phx.gen.auth ↔ mix igniter.install ash_authentication ash_authentication_phoenix --auth-strategy magic_link。建置那步已經跑過了, 它產生了 User、Token 這兩個 resource 跟路由。mix phx.gen.live ↔ mix ash_phoenix.gen.live。概念一樣, 差別是它讀一個已存在的 resource, 而不是吃一串欄位清單。Phoenix + Ecto: mix ecto.gen.migration add_posts 建一個空檔案, create table 我們自己寫。mix ecto.migrate 跑, mix ecto.rollback 退, mix ecto.migrations 看狀態。
Ash: mix ash.codegen add_posts 從 resource 寫出 migration。它把每個 resource 跟 priv/resource_snapshots/ 裡上一次的快照比對, 把差異寫出來:
$ mix ash.codegen create_posts
Getting extensions in current project...
Running codegen for AshPostgres.DataLayer...
Please always manually review the generated migrations for correctness.
* creating priv/repo/migrations/20260915164615_create_posts.exs
* creating priv/resource_snapshots/repo/posts/20260915164616.json
def up do
create table(:posts, primary_key: false) do
add :id, :uuid, null: false, default: fragment("gen_random_uuid()"), primary_key: true
add :title, :text, null: false
add :body, :text
add :inserted_at, :utc_datetime_usec,
null: false,
default: fragment("(now() AT TIME ZONE 'utc')")
add :updated_at, :utc_datetime_usec,
null: false,
default: fragment("(now() AT TIME ZONE 'utc')")
end
end
allow_nil? false 變成了 null: false。這就是一個普通的 Ecto migration, 看過後如果需要的話可以改, 然後再執行:
mix ash.migrate # 每個 data layer 的 ecto.migrate
mix ash.rollback # ecto.rollback
mix ash.codegen --check # resource 跟快照不一致就失敗, 給 CI 用
mix ecto.migrations 一樣看得到狀態:
Status Migration ID Migration Name
--------------------------------------------------
up 20260915164458 initialize_and_add_authentication_resources_and_add_magic_link_auth_extensions_1
up 20260915164459 initialize_and_add_authentication_resources_and_add_magic_link_auth
up 20260915164615 create_posts
從現在開始, 改一個 attribute 就是流程就變成:
改 resourcemix ash.codegen 名字
檢查檔案,mix ash.migrate
Phoenix + Ecto: 在 mix.exs 加 {:pkg, "~> x"}, mix deps.get, 然後照 README 手動加 config 跟 router 的那幾行。
Ash: mix igniter.install pkg。它加依賴、抓下來, 然後跑那個套件的 installer, 由 installer 幫你改 mix.exs、config/*.exs、router 等等。建置那步就是這樣跑了七次。兩個相關的 task:
mix ash.extend DemoCms.Content.Post json_api 幫既有的 resource 或 domain 加一個 extension。mix igniter.upgrade 是 mix deps.update 加上套件附的升級腳本。mix phx.server、iex -S mix、mix test、mix format: 這些沒變。
mix format 現在多跑一個 Spark formatter plugin, 所以產生的 resource 使用 ash 風格的寫法不會有括號
mix phx.routes 也沒變, 而且看得到 auth installer 加了什麼:
GET / DemoCmsWeb.PageController :home
GET /auth/user/magic_link DemoCms.Accounts.User.magic_link :accept
POST /auth/user/magic_link/request DemoCms.Accounts.User.magic_link :request
POST /auth/user/magic_link DemoCms.Accounts.User.magic_link :sign_in
GET /sign-out AshAuthentication.Phoenix.SignOutLive :sign_out
GET /sign-in AshAuthentication.Phoenix.SignInLive :sign_in
GET /register AshAuthentication.Phoenix.SignInLive :register
GET /magic_link/:token AshAuthentication.Phoenix.MagicSignInLive :sign_in
GET /admin/*route AshAdmin.PageLive :page
...
mix ash_authentication.phoenix.routes 只印 auth 相關的。
在 iex 裡, Ecto 的 Repo.insert(%Post{} |> Post.changeset(attrs)) 對應到 Ash 的 Ash.create(Post, attrs):
iex> Ash.create!(DemoCms.Content.Post, %{title: "Hello, Ash", body: "First post."})
%DemoCms.Content.Post{
id: "882bbad6-760d-4728-9c58-ec34e23100c8",
title: "Hello, Ash",
body: "First post.",
...
}
iex> Ash.read!(DemoCms.Content.Post) |> Enum.map(& &1.title)
["Hello, Ash"]
iex> DemoCms.Repo.query!("select title from posts").rows
[["Hello, Ash"]]
資料就是普通的 Postgres 資料。Repo.insert 跟 Repo.all 也還能用, 不過現在對我們來說他們是底層
比較少直接使用
mix ash_postgres.gen.resources 從既有的資料庫 schema 產生 resourcemix ash.generate_resource_diagrams 幫每個 domain 畫一張 Mermaid 的實體關係圖mix ash.generate_policy_charts 把一個 resource 的授權 policy 畫成流程圖mix ash.generate_livebook 幫每個 domain 產生一本 Livebook, 把 resource 文件化