Elixir 진영에 Ash Framework 라는 아주 흥미로운 프레임워크가 있다.
간단하게 알아보자.
Ash?
Model your domain, derive the rest
공식 홈페이지에서 Model 만 만들면 나머지는 알아서 해주겠다고 한다.
어떻게 한다는건지 알아보자.
Resource
리소스는 내가 해결하고자 하는 문제 도메인을 모델링한 결과이다. 일반적인 ORM 과 같이 DB 와 연결해서 사용할 수있지만 DB 와 연동하지 않고 사용하는 데에도 문제는 없다.
CRUD
Resource 를 정의하면 CRUD 를 알아서 만들어 준다.
물론 아주 약간의 코드는 작성해야 한다.
defmodule Tutorial.Profile do use Ash.Resource, domain: Tutorial, data_layer: Ash.DataLayer.Ets actions do create :create do accept [:name] end update :update do accept [:name] end defaults [:read, :destroy] end attributes do uuid_primary_key :id attribute :name, :string end end defmodule Tutorial do use Ash.Domain resources do resource Tutorial.Profile end end
위 코드블록에서 attributes 에 Resource 를 구성하는 것들을 정의한다.
현재는 단순히 PK 와 name 만 가지는 Profile 이라는 Resource 를 정의했다.
actions 에는 Resource 로 수행 가능한 것들을 정의한다.
create 는 resource 를 생성하는 action 들을 정의한다.
:create 라는 action 은 name 프로퍼티만 받아서 생성한다.
update 도 name 만 받아서 수정한다.
defaults 는 read, destroy 는 ash 의 기본 기능을 사용한다는 의미이다.
위와같이 정의하기만 하면 아래와 같이 사용 가능하다.
# CREATE Tutorial.Profile |> Ash.Changeset.for_create(:create, %{name: "The Name"}) |> Ash.create!()
# Profile 전체조회 Tutorial.Profile |> Ash.read!()
# Filter 조회 [joe] = Tutorial.Profile |> Ash.Query.filter(name == "Joe Armstrong") |> Ash.read!()
# UPDATE joe |> Ash.Changeset.for_update(:update, %{name: "Neil Armstrong"}) |> Ash.update!()
# 삭제 (destroy) Ash.destroy!(joe)
아래는 조금 더 복잡한 Model 예시이다.
defmodule Tutorial.Support.Ticket do use Ash.Resource, domain: Tutorial.Support, data_layer: Ash.DataLayer.Ets actions do defaults [:read] create :open do accept [:subject, :description, :representative_id] end update :close do accept [] change set_attribute(:status, :closed) end end attributes do uuid_primary_key :id attribute :subject, :string, allow_nil?: false attribute :description, :string attribute :status, :atom do constraints one_of: [:open, :closed] default :open allow_nil? false end create_timestamp :created_at update_timestamp :updated_at end relationships do belongs_to :representative, Tutorial.Support.Representative end end defmodule Tutorial.Support.Representative do use Ash.Resource, domain: Tutorial.Support, data_layer: Ash.DataLayer.Ets actions do defaults [:read] create :create do accept [:name] end end attributes do uuid_primary_key(:id) attribute(:name, :string) end end
Ticket 이라는 리소스와 Representative 라는 resource 가 연관관계를 맺고있는 모델링이다.
아래와 같이 연관관계 설정이 가능하다.
추가로 Ash.load 를 사용하면 연관관계 매핑을 한 리소스를 조회할 수 있다.
load 처럼 명시적으로 어떤 연관관계를 불러올건지 서술하는 방식이 마음에 든다.
joe = Tutorial.Support.Representative |> Ash.Changeset.for_create(:create, %{name: "Joe Armstrong"}) |> Ash.create!() ticket = Tutorial.Support.Ticket |> Ash.Changeset.for_create(:create, %{subject: "My spoon is too big!", representative_id: joe.id}) |> Ash.create!() |> Ash.load!([:representative])
이후 ticket.representative 를 찍어보면 아래와 같이 조회된다.
#Tutorial.Support.Representative< __meta__: #Ecto.Schema.Metadata<:loaded>, id: "50018f60-c3ea-4535-bbdf-c5e30252dd1c", name: "Joe Armstrong", aggregates: %{}, calculations: %{}, ... >
만약 load 를 하지 않은 경우라면?
#Ash.NotLoaded<:relationship, field: :representative>
위와 같이 조회된다.
Aggregates
집계와 관련된 함수를 처리하는 로직도 custom 하게 작성할 수 있다.
defmodule Tutorial.Support.Representative do use Ash.Resource, domain: Tutorial.Support, data_layer: Ash.DataLayer.Ets actions do defaults [:read] create :create do accept [:name] end end attributes do uuid_primary_key :id attribute :name, :string end relationships do has_many :tickets, Tutorial.Support.Ticket end code_interface do define :create, args: [:name] end aggregates do count :count_of_open_tickets, :tickets do filter expr(status == :open) end end end
aggregates do count :count_of_open_tickets, :tickets do filter expr(status == :open) end end
위 코드는 Representative.count_of_open_tickets 라는 속성을 tickets 에서 open 상태인 것들만 필터링해서 갯수를 노출하게 해준다.
아래와 같이 사용 가능하다.
joe = Ash.load!(joe, [:count_of_open_tickets]) joe.count_of_open_tickets
이렇게까지만 보면 사실상 Spring 의 Data JPA 정도의 역할만 하는 것으로 보인다.
하지만 Ash 는 훨씬 더 큰 역할을 하는 프레임워크이다.
다른 기능들은 다음 포스팅에서 더 알아보자.