VOOZH about

URL: https://dev.to/adamanq/ash-framework-znakomstvo-g0g

⇱ Ash Framework: Знакомство - DEV Community


Когда новичок приходит в Elixir и Phoenix, ему почти всегда первым делом показывают Ecto. Создаём схемы, пишем changeset’ы, выносим запросы в контексты. Подход гибкий, но с ростом проекта начинает проявляться серьёзная проблема 😟

Бизнес-логика расползается по десяткам файлов. В итоге возникает путаница относительно того, куда именно поместить код обработки запросов и как разобраться в существующем беспорядке, а через год-два сложно понять, где находится «правильное» место для кода, а новые разработчики тратят недели на погружение в проект

Ash Framework создан именно для решения этой боли 💪

Важное уточнение: Ash — это не замена Phoenix и не новый веб-фреймворк. Это мощный декларативный слой, который организует бизнес-логику и данные поверх существующей экосистемы.




📖 Предисловие

Важно понимать, что цель этой статьи вовсе не состоит в том, чтобы уговорить вас использовать Ash повсеместно и постоянно!

Сам я люблю Ash именно из-за того, что он даёт возможность собрать всю необходимую логику в едином пространстве, где ей самое место. Этот инструмент значительно упрощает работу, уменьшая количество шаблонного кода и освобождая от утомительной реализации стандартных CRUD.

Если готовы ознакомиться с таким инструментом, но собираетесь прибегать к нему лишь тогда, когда это действительно оправдано — смело читайте дальше. Ash — это не универсальное лекарство от всех проблем, а всего лишь удобный набор полезных инструментов.




🚀 Что такое Ash?

Ash — это фреймворк для быстрой и эффективной разработки приложений на Elixir. Его цель заключается в облегчении запуска новых проектов и обеспечении удобства поддержки существующих решений, гарантируя отличную производительность и стабильность даже после длительного периода активной разработки.

Ключевая идея Ash звучит так:

Точно сформулируй структуру своей бизнес-логики, а всю остальную работу (миграция базы данных, создание API-интерфейсов, валидация данных, управление доступом, оформление форм) возьмет на себя Ash

Фреймворк прекрасно взаимодействует с такими популярными инструментами, как Phoenix, PostgreSQL, GraphQL и LiveView.




🔑 Ключевые абстракции Ash

Всё в Ash строится вокруг трёх основных понятий:

Давайте разберём каждую концепцию подробно!




🧩 Что такое Ресурс?

Resource — это один модуль, в котором полностью описана одна бизнес-сущность вашего приложения (Пользователь, Урок, Заказ, Пост, Платёж и т.д.).

В нём в одном месте собрано всё, что касается этой сущности:

  • Структура данных (поля)
  • Связи с другими сущностями
  • Операции, которые можно выполнять (CRUD + кастомные)
  • Правила валидации
  • Авторизация (кто и что может делать)
  • Вычисляемые поля
  • Индексы и ограничения
  • Настройки хранения в базе

Resource в Ash объединяет в одном модуле то, что в классическом Elixir/Phoenix обычно разносится по нескольким частям: Ecto Schema, Changesets, Context-функции, а также логика авторизации и вычисляемые поля.


Resource — сердце Ash

Ash построен вокруг идеи Resource-Oriented Design. Ты описываешь что представляет собой сущность и что с ней можно делать — а Ash сам генерирует остальное: SQL-запросы, API, формы, миграции, валидацию и т.д.


Полный пример современного Resource

defmodule CourseHub.Courses.Lesson do
 use Ash.Resource,
 otp_app: :course_hub, # Привязка к приложению
 domain: CourseHub.Courses, # В каком домене живёт
 data_layer: AshPostgres.DataLayer # Где храним данные

 # ==================== ХРАНЕНИЕ ====================
 postgres do
 table "lessons"
 repo CourseHub.Repo
 end

 # ==================== ПОЛЯ (Attributes) ====================
 attributes do
 uuid_primary_key :id

 attribute :title, :string do
 public? true # видно в API и формах
 allow_nil? false
 constraints min_length: 3, max_length: 200
 end

 attribute :description, :string, public?: true
 attribute :content, :string, public?: true

 attribute :status, :atom do
 public? true
 default? :draft
 constraints one_of: [:draft, :published, :archived]
 end

 create_timestamp :inserted_at
 update_timestamp :updated_at
 end

 # ==================== СВЯЗИ (Relationships) ====================
 relationships do
 belongs_to :author, CourseHub.Accounts.User do
 public? true
 allow_nil? false
 end

 has_many :comments, CourseHub.Courses.Comment do
 public? true
 end

 many_to_many :tags, CourseHub.Courses.Tag do
 public? true
 through CourseHub.Courses.LessonTag
 source_attribute_on_join_resource :lesson_id
 destination_attribute_on_join_resource :tag_id
 end
 end

 # ==================== ДЕЙСТВИЯ (Actions) ====================
 actions do
 defaults [:create, :read, :update, :destroy]

 # Кастомное чтение
 read :published do
 filter expr(status == :published)
 pagination offset?: true, countable: true
 prepare build(sort: [inserted_at: :desc])
 end

 # Кастомное создание с логикой
 create :publish do
 argument :reason, :string
 change set_attribute(:status, :published)
 end

 # Полностью кастомное действие
 action :calculate_reading_time, :integer do
 argument :content, :string, allow_nil?: false
 run fn input, _context ->
 minutes = String.length(input.arguments.content) |> div(500) |> max(1)
 {:ok, minutes}
 end
 end
 end

 # ==================== ВЫЧИСЛЯЕМЫЕ ПОЛЯ ====================
 calculations do
 calculate :reading_time_minutes, :integer do
 calculation fn records, _context ->
 Enum.map(records, fn lesson ->
 (String.length(lesson.content || "") / 500) |> Float.ceil() |> trunc()
 end)
 end
 end
 end

 # ==================== ИНДЕКСЫ ====================
 identities do
 identity :unique_title_per_author, [:title, :author_id]
 end

 # ==================== АВТОРИЗАЦИЯ ====================
 policies do
 policy action_type(:read) do
 authorize_if always()
 end

 policy action(:update) do
 authorize_if expr(author_id == ^actor(:id))
 end
 end
end


Основные разделы Resource

Раздел Что описывает Аналог в обычном Elixir/Phoenix
attributes Поля и их типы + валидация Ecto Schema + поля
relationships Связи с другими ресурсами Ecto associations
actions Все возможные операции (CRUD + кастомные) Context-функции + Changesets
calculations Вычисляемые поля (на лету) Virtual fields / функции
aggregates Агрегаты (count, sum, avg и т.д.) Запросы с group_by
identities Уникальные индексы Ecto unique constraints
policies Правила авторизации Ручная проверка прав
postgres Настройки хранения в PostgreSQL Миграции


Ключевые особенности Resource

  1. Всё в одном файле — огромный плюс для понимания сущности.
  2. Introspection — Ash может в runtime читать всю информацию о ресурсе (поля, действия, политики). Это используется для генерации API, форм, админок.
  3. Декларативность — ты описываешь что должно быть, а не как это реализовать.
  4. Расширяемость — можно добавлять свои changes, preparations, validations, notifiers.
  5. Embedded Resources — можно создавать ресурсы, которые хранятся не в отдельной таблице, а как JSON/Map внутри другого ресурса.


Когда создавать новый Resource?

Практически всегда, когда появляется новая бизнес-сущность (таблица или даже сложный JSON-объект).




🛠️ Подробнее об Actions (Действиях)

Action (Действие) — это явно объявленная операция, которую можно выполнить над ресурсом.

Всё взаимодействие с данными в Ash идёт только через Actions. Нельзя просто взять запись и изменить её вручную — всегда вызываешь действие.

Это заменяет:

  • Обычные функции в Context’ах Phoenix
  • Changesets в Ecto
  • Контроллеры и сервисные слои


Пять типов Actions

Тип действия Что делает? В транзакции? Что возвращает?
create Создаёт новую запись Да Новую запись ресурса
🔍 read Читает одну запись или целый список Нет (можно включить) Запись или список записей
✏️ update Обновляет существующую запись Да Обновлённую запись
🗑️ destroy Удаляет запись Да Удалённую запись
action (Generic) Любая кастомная бизнес-логика Нет (можно включить) Любое значение (что угодно)


Полный пример всех типов Actions

defmodule CourseHub.Courses.Lesson do
 use Ash.Resource, ...

 actions do
 # 1. Стандартные CRUD (defaults)
 defaults [:create, :read, :update, :destroy]

 # 2. Кастомное Read
 read :published do
 filter expr(status == :published)
 pagination offset?: true, countable: true, default_limit: 20
 prepare build(sort: [inserted_at: :desc], load: [:author, :comment_count])
 end

 read :for_user do
 argument :user_id, :uuid, allow_nil?: false
 filter expr(author_id == ^arg(:user_id))
 end

 # 3. Кастомное Create
 create :publish do
 argument :reason, :string, allow_nil?: true

 # Изменения (changes)
 change set_attribute(:status, :published)
 change set_attribute(:published_at, &DateTime.utc_now/0)
 change {CourseHub.Changes.NotifySubscribers, topic: "new_lessons"}

 # Хуки
 after_action fn _changeset, lesson, _context ->
 # side-effect после успешного создания
 {:ok, lesson}
 end
 end

 # 4. Кастомное Update
 update :archive do
 accept [:archive_reason] # какие поля можно менять
 change set_attribute(:status, :archived)
 change set_attribute(:archived_at, &DateTime.utc_now/0)
 end

 # 5. Generic Action (полностью кастомное)
 action :calculate_reading_time, :integer do
 argument :content, :string, allow_nil?: false
 argument :words_per_minute, :integer, default?: 200

 run fn input, _context ->
 minutes = String.length(input.arguments.content) 
 |> div(input.arguments.words_per_minute) 
 |> max(1)
 {:ok, minutes}
 end
 end
 end
end


Как вызывать Actions

# Через домен (рекомендуемый способ)
CourseHub.Courses.publish!(lesson, %{reason: "Готово"})

CourseHub.Courses.read!(CourseHub.Courses.Lesson, :published)

# Или с Ash.Query / Ash.Changeset для сложных случаев
query = Ash.Query.for_read(Lesson, :published)
Ash.read!(query)


Основные возможности Actions

1. argument — входные параметры

Позволяет передавать данные в действие и валидировать их.

argument :title, :string, allow_nil?: false, constraints: [min_length: 5]

argument :published_at, :datetime, default?: &DateTime.utc_now/0

argument :user_id, :uuid, allow_nil?: false

2. accept — какие атрибуты можно менять

Ограничивает список полей, которые разрешено изменять при create или update. Очень полезно для безопасности.

create :register do
 accept [:email, :password, :name] # только эти поля можно передать
end

update :update_profile do
 accept [:name, :bio, :avatar_url]
end

3. change — трансформации данных

Самая мощная и часто используемая часть Actions. Здесь происходит основная бизнес-логика.

create :publish do
 argument :reason, :string

 # Встроенные изменения
 change set_attribute(:status, :published)
 change set_attribute(:published_at, &DateTime.utc_now/0)

 # Кастомный change-модуль
 change {CourseHub.Changes.NotifySubscribers, topic: "lessons"}

 # Анонимная функция
 change fn changeset, _context ->
 Ash.Changeset.change_attribute(changeset, :slug, generate_slug(changeset))
 end
end

4. prepare — подготовка запроса (в основном для read)

Позволяет заранее настроить запрос: сортировку, предзагрузку связей, дополнительные фильтры и т.д.

read :published do
 filter expr(status == :published)

 prepare build(
 sort: [inserted_at: :desc],
 load: [:author, :comments, :reading_time_minutes],
 limit: 20
 )
end

5. Хуки (before/after)

Позволяют выполнить код до или после выполнения действия.

create :publish do
 before_action fn changeset, _context ->
 # проверка перед выполнением
 validate_business_rules(changeset)
 end

 after_action fn _changeset, lesson, _context ->
 # side-effect после успешного создания
 LessonNotifier.publish_event(lesson)
 {:ok, lesson}
 end

 after_transaction fn 
 {:ok, lesson}, _context -> broadcast_new_lesson(lesson)
 {:error, _}, _ -> :ok 
 end
end

6. transaction? — управление транзакцией

Определяет, нужно ли оборачивать действие в базу данных транзакцию.

action :transfer_money, :boolean do
 argument :from_account_id, :uuid
 argument :to_account_id, :uuid
 argument :amount, :decimal

 transaction? true # будет выполнено в одной транзакции

 run fn input, _context ->
 # сложная операция с несколькими записями
 {:ok, true}
 end
end

7. pipe_through — переиспользование логики

Позволяет создавать общие пайплайны и подключать их к разным действиям (очень полезно для DRY).

# В Resource
pipelines do
 pipeline :ensure_admin do
 change ensure_actor_role(:admin)
 validate actor_role(:admin)
 end
end

create :create_admin_content do
 pipe_through :ensure_admin
 accept [:title, :content]
end

update :update_admin_content do
 pipe_through :ensure_admin
 accept [:title, :content]
end


Почему Actions — это круто?

  • Полная типизация и introspection — Ash знает всё о действии в runtime (используется для GraphQL, форм, админок)
  • Декларативность — код читается как документация бизнеса
  • Повторное использование — одно действие можно использовать в LiveView, API, Background Job, Tests
  • Безопасность — политики авторизации привязываются к конкретным действиям
  • Тестируемость — легко тестировать отдельные действия


Сравнение с классическим Elixir/Phoenix

Подход Где логика? Кол-во файлов Понятность
Ecto + Context Context + Changeset 4–8 Средняя
Ash Actions В одном Resource 1 Высокая

Actions — это то место, где живёт вся бизнес-логика твоего ресурса




🏢 Что такое Domain (Домены)?

Domain (Домен) — это логический контейнер, который объединяет группу связанных Resources (ресурсов).

Он похож на Phoenix Context, но гораздо мощнее. Если Resource — это описание одной сущности (как User, Lesson, Order), то Domain — это «модуль» или «bounded context» (в терминах DDD), который собирает вместе несколько сущностей одной бизнес-области.

Пример:

  • Accounts — домен для пользователей, профилей, авторизации
  • Courses — домен для курсов, уроков, тестов, enrollments
  • Billing — домен для платежей, подписок, инвойсов
  • Therapy — домен для терапии, разговоров, сообщений (из реальных примеров)


Три главные цели Domain

  1. Группировка ресурсов

    Организация проекта. Вместо того чтобы все ресурсы были разбросаны, они явно лежат в логических группах.

  2. Централизованный Code Interface

    Domain позволяет определять удобные функции-обёртки для вызова действий ресурсов.

  3. Общие настройки и cross-cutting concerns

    Здесь можно задать поведение, которое применяется ко всем ресурсам домена (таймауты, пагинация по умолчанию, авторизация и т.д.).


Как выглядит Domain на практике

defmodule CourseHub.Courses do
 use Ash.Domain, otp_app: :course_hub

 # 1. Группировка ресурсов
 resources do
 resource CourseHub.Courses.Course
 resource CourseHub.Courses.Lesson
 resource CourseHub.Courses.Quiz
 resource CourseHub.Courses.Enrollment
 end

 # 2. Code Interface — удобные функции
 define :get_lesson, action: :read, args: [:id], get?: true
 define :list_published_courses, action: :published
 define :enroll_user, action: :create, resource: CourseHub.Courses.Enrollment

 # 3. Общие настройки
 execution do
 timeout :timer.seconds(30) # глобальный таймаут
 default_page_size 20
 end

 # Включение AshAdmin (админка)
 admin do
 show? true
 end
end


Зачем нужен Domain?

  • Организация и границы. Чётко разделяет разные части приложения (как bounded contexts в DDD). Accounts не знает про Billing без явного отношения

  • Удобный публичный API. Вместо Ash.read!(CourseHub.Courses.Lesson, ...) ты пишешь красиво:

 CourseHub.Courses.get_lesson!(lesson_id)
 CourseHub.Courses.list_published_courses!()
  • Централизованное управление. Можно отключить авторизацию для всего домена в dev-режиме, задать общие политики, notifiers и т.д.

  • Introspection и расширения. Многие расширения Ash (GraphQL, JsonApi, Phoenix и т.д.) работают на уровне домена

  • Тестирование и изоляция. Легче тестировать одну бизнес-область целиком


Лучшие практики при работе с доменом

  • Один ресурс — один домен. Ресурс не может принадлежать нескольким доменам одновременно.

  • Сколько доменов делать?

    • Маленький проект → 1–3 домена.
    • Средний/большой → по одному на крупную бизнес-область (Accounts, Core, Billing, Notifications и т.д.). Это помогает в масштабировании и понимании архитектуры.
  • Code Interface — очень мощная фича. Ты можешь определять не только простые обёртки, но и с предустановленными фильтрами, load'ами и т.д.


Сравнение: Domain vs Resource

Аспект Resource Domain
Что описывает Одну сущность Группу сущностей
Где лежит логика Атрибуты, действия, политики Группировка + общие настройки
Вызов кода Через домен Публичный интерфейс приложения
Аналог в Phoenix Schema + Context (частично) Phoenix Context
Количество на проект Много (по одной на таблицу) Мало (по бизнес-областям)


Почему Domain особенно полезен?

  • Проект растёт > 10–15 ресурсов
  • Нужна сложная авторизация (policies)
  • Делаешь GraphQL / JSON:API / Admin-панель
  • Командная разработка — всем нужно понимать границы




⚔️ Ash + Phoenix vs Ecto + Phoenix

Аспект Phoenix + Ecto (классика) 😐 Phoenix + Ash 🔥 Кто выигрывает?
Организация бизнес-логики Разбросана по Context, Changeset, отдельным модулям Всё в одном Resource — атрибуты, действия, политики, связи Ash (всё на виду)
CRUD-операции Пишешь вручную: create, read, update, delete + boilerplate defaults [:create, :read, :update, :destroy] — и готово ✨ Ash (1 строка)
Авторизация Ручные проверки / Bodyguard / scopes Декларативные Policies (secure by default) ⚡ Ash (автоматика)
Вычисляемые поля Virtual fields + функции в Context Calculations — автоматически подгружаются и кэшируются Ash
Количество файлов на сущность 3–6+ файлов (Schema + Context + Changeset + Policy) 1 файл — Resource Ash
Генерация API Absinthe / GraphQL вручную + много кода AshGraphql или AshJsonApi — добавил расширение и готово Ash
LiveView формы Phoenix.Component + ручная валидация AshPhoenix.Form — формы сами знают всё Ash
Multi-tenancy Пишешь сам (или используешь схемы) Встроено в Policies (attribute-based или context-based) Ash
Поддерживаемость через 2–3 года Средняя (логика расползается) Высокая (всё декларативно и в одном месте) Ash
Скорость разработки Нормальная на старте Значительно быстрее после кривой обучения 🚀 Ash (после обучения)
Гибкость Максимальная (полный контроль) Очень высокая + можно escape в чистый Ecto когда нужно Ничья
Кривая обучения Легче для новичков Круче, но окупается в большом проекте Ecto

Что стоит выбрать в 2026 году?

Плюсы Ash 🔥

  • Декларативность → «Опиши домен — остальное Ash выведет сам»
  • Сокращение шаблонного кода (часто встречаются отзывы о Ash вроде: «я создаю программы, используя в 3–5 раз меньше строк»)
  • Автоматическая генерация GraphQL, JSON:API, Admin-панели, аутентификации
  • Политики авторизации и multi-tenancy из коробки
  • Отличная поддерживаемость на длинной дистанции (именно то, о чём мечтают в продакшене)

Плюсы классического Phoenix + Ecto 😐

  • Полный контроль над каждым байтом кода
  • Проще для совсем маленьких проектов и пет-проектов
  • Нет дополнительной абстракции (если ты любишь «голый» Elixir)
  • Легче для новичков в Elixir

Когда выбирать что?

  • Маленький пет-проект или эксперимент → бери чистый Phoenix + Ecto
  • Средний/крупный проект, SaaS, долгоживущее приложение → Phoenix + Ash (экономия времени и нервов огромная)
  • Хочешь и то, и другое → можно спокойно миксовать в одном проекте (Ash и обычный Ecto отлично уживаются)


😴 В заключение

Ash — это не просто библиотека, а изменение подхода к разработке на Elixir. Он особенно хорошо заходит в проектах среднего и крупного размера, где важны чистота архитектуры, скорость разработки и долгосрочная поддерживаемость.

Конечно, у него есть кривая обучения, и в совсем простых пет-проектах он может быть избыточен. Но если вы устали от расползающейся логики и хотите чёткой, декларативной структуры — Ash дарит очень приятное ощущение контроля над кодом.




🥴 От автора

Спасибо большое за интерес к этой статье! Надеюсь, она помогла разобраться, что представляет собой Ash и зачем он используется.

Если эта статья пришлась вам по душе и хочется ещё материалов такого плана, присоединяйтесь ко мне в моём телеграм-канале, где выкладываю обзоры книг, публикации по Elixir, переводы технической литературы и интересные новости!