返回 Skill 列表
extension
分类: 开发与工程无需 API Key

ash-framework

全面的Ash框架指南,适用于Elixir应用程序。在处理Ash资源、领域、操作、查询、更改集、策略、计算或聚合时使用。涵盖了代码接口、错误处理、验证、更改、关系和授权等内容。在使用Ash功能之前,请阅读文档 - 不要假设已有相关知识。

person作者: jakexiaohubgithub

Ash Framework Guidelines

Ash is a declarative framework for modeling domains with resources. Read documentation before using features.

Code Interfaces

Define code interfaces on domains - avoid direct Ash.get!/2 calls in LiveViews/Controllers:

# In domain
resource Post do
  define :get_post, action: :read, get_by: [:id]
  define :list_posts, action: :read
  define :create_post, action: :create, args: [:title]
end

# Usage - prefer query option over manual Ash.Query building
posts = MyApp.Blog.list_posts!(
  query: [filter: [status: :published], sort: [published_at: :desc], limit: 10],
  load: [author: :profile, comments: [:author]]
)

post = MyApp.Blog.get_post!(id, load: [comments: [:author]])

Authorization functions are auto-generated: can_create_post?(actor), can_update_post?(actor, post).

Using scopes: Pass scope: socket.assigns.scope in LiveViews; use context parameter in hooks.

Actions

  • Create specific, well-named actions (not generic CRUD)
  • Put business logic inside action definitions
  • Use before_action/after_action for same-transaction logic
  • Use before_transaction/after_transaction for external calls
actions do
  create :sign_up do
    argument :invite_code, :string, allow_nil?: false
    change set_attribute(:joined_at, &DateTime.utc_now/0)
    change relate_actor(:creator)
  end
end

Querying

Important: Ash.Query.filter/2 is a macro - you must require Ash.Query:

require Ash.Query

Post
|> Ash.Query.filter(status == :published)
|> Ash.Query.sort(published_at: :desc)
|> Ash.Query.load([:author, :comments])
|> Ash.Query.limit(10)
|> Ash.read!()

Error Handling

  • Use ! variations (Ash.create!, Domain.action!) when expecting success
  • Prefer ! over pattern matching {:ok, result} = ...
  • Error classes: Forbidden > Invalid > Framework > Unknown

Validations

# Built-in validations
validate compare(:age, greater_than_or_equal_to: 18)
validate match(:email, ~r/@/)
validate one_of(:status, [:active, :pending])

# Conditional validation
validate present(:phone) do
  where eq(:contact_method, "phone")
end

# Skip if prior validations failed
validate expensive_check() do
  only_when_valid? true
end

Avoid redundant validations - don't duplicate attribute constraints (allow_nil? false already validates presence).

Changes

# Built-in changes
change set_attribute(:status, "pending")
change relate_actor(:creator)
change atomic_update(:counter, expr(counter + 1))

# Custom change module
defmodule MyApp.Changes.Slugify do
  use Ash.Resource.Change

  def change(changeset, _opts, _context) do
    title = Ash.Changeset.get_attribute(changeset, :title)
    slug = title |> String.downcase() |> String.replace(~r/[^a-z0-9]+/, "-")
    Ash.Changeset.change_attribute(changeset, :slug, slug)
  end
end

Prefer custom modules over anonymous functions for changes, validations, preparations.

Preparations

Modify queries before execution:

prepare build(sort: [created_at: :desc])
prepare build(filter: [deleted: false])

# Conditional preparation
prepare build(filter: [visible: true]) do
  where argument_equals(:include_hidden, false)
end

Data Layers

use Ash.Resource,
  domain: MyApp.Blog,
  data_layer: AshPostgres.DataLayer  # or :embedded, Ash.DataLayer.Ets

postgres do
  table "posts"
  repo MyApp.Repo
end

Migrations

Run mix ash.codegen <name> after modifying resources. Use --dev during development, final name at end.

Testing

  • Test through code interfaces
  • Use authorize?: false when auth isn't the focus
  • Use Ash.can? to test policies
  • Use raising ! functions

Prevent deadlocks - use unique values for identity fields in concurrent tests:

%{email: "test-#{System.unique_integer([:positive])}@example.com"}

References