AshPostgres 사용해 보기 (2)

지난 글에 이어서 AshPostgres 튜토리얼을 따라가며 사용법을 배워 본다. 이번 글 에서는 집계(Aggregates)와 계산(Calculations)에 대해 알아 볼 것이다.

집계(Aggregates)

집계(Aggregates)는 관계로 연결된 데이터들을 모아 요약 정보를 제공하는 도구이다. 관계로 연결된 데이터들의 합계, 수, 최대값, 최소값, 평균값 등 통계성 값 들을 얻는데 사용한다. 예제에선 Representative 와 Ticket 이 일대다 관계로 연결되었기 때문에 담당자가 몇개의 티켓을 가지고 있는지 진행중인 티켓은 몇 개 인지 같은 값을 얻는데 사용할 수 있다.

다음 코드를 담당자 리소스 선언에 추가한다.

# in lib/helpdesk/support/representative.ex

  aggregates do
    # 첫번째 인자는 집계의 이름, 두 번째 인자는 관계의 이름
    count :total_tickets, :tickets

    count :open_tickets, :tickets do
      # 표현식으로 필터링 할 수 도 있다
      filter expr(status == :open)
    end

    count :closed_tickets, :tickets do
      filter expr(status == :closed)
    end
  end
# in iex

iex> require Ash.Query

iex> Helpdesk.Support.Representative
|> Ash.Query.filter(closed_tickets < 4)  # closed_tickets 집계가 사용 됨
|> Ash.Query.sort(closed_tickets: :desc) # closed_tickets 집계가 사용 됨
|> Ash.read!()

18:44:34.441 [debug] QUERY OK source="representatives" db=15.7ms queue=0.1ms idle=1406.0ms
SELECT r0."id", r0."name" FROM "representatives" AS r0 LEFT OUTER JOIN LATERAL (SELECT st0."representative_id" AS "representative_id", coalesce(count(*), $1::bigint)::bigint AS "closed_tickets" FROM "public"."tickets" AS st0 WHERE (r0."id" = st0."representative_id") AND (st0."status"::varchar = $2::varchar) GROUP BY st0."representative_id") AS s1 ON TRUE WHERE (coalesce(s1."closed_tickets", $3::bigint)::bigint < $4::bigint) ORDER BY coalesce(s1."closed_tickets", $5::bigint)::bigint DESC [0, :closed, 0, 4, 0]

# 저장된 데이터 상황에 따라 결과는 달라질 수 있음
[
  %Helpdesk.Support.Representative{
    id: "f773d41f-24f8-4d38-a5ef-a98a187de034",
    name: "Joe Armstrong",
    total_tickets: #Ash.NotLoaded<:aggregate, field: :total_tickets>,
    open_tickets: #Ash.NotLoaded<:aggregate, field: :open_tickets>,
    closed_tickets: #Ash.NotLoaded<:aggregate, field: :closed_tickets>,
    tickets: #Ash.NotLoaded<:relationship, field: :tickets>,
    __meta__: #Ecto.Schema.Metadata<:loaded, "representatives">
  },
  %Helpdesk.Support.Representative{
    id: "a4467ac7-6f4e-49b1-bdea-9dc98acb5ac9",
    name: "Test User 2",
    total_tickets: #Ash.NotLoaded<:aggregate, field: :total_tickets>,
    open_tickets: #Ash.NotLoaded<:aggregate, field: :open_tickets>,
    closed_tickets: #Ash.NotLoaded<:aggregate, field: :closed_tickets>,
    tickets: #Ash.NotLoaded<:relationship, field: :tickets>,
    __meta__: #Ecto.Schema.Metadata<:loaded, "representatives">
  }
]

리소스에 total_tickets, open_tickets, closed_tickets 값이 추가된 것을 확인할 수 있다. 기본적으로 명시적 로드를 해야만 쿼리에도 반영되고 값도 획득된다. closed_tickets 값은 필터링에서 사용되었기 때문에 쿼리에 집계가 포함되었으나 해당 값 자체는 로드하지 않았기 때문에 NotLoaded 이다. 명시적으로 값을 로드하는 코드는 다음과 같다.

iex> Helpdesk.Support.Representative 
   |> Ash.Query.filter(closed_tickets < 4) 
   |> Ash.Query.sort(closed_tickets: :desc) 
   |> Ash.Query.load([:closed_tickets, :open_tickets, :total_tickets]) # 명시
   |> Ash.read!()

계산(Calculations)

계산은 집계와 비슷하지만 개별 레코드에 대해 작동한다는 것이 다르다. 그러나 계산은 집계된 값을 참조할 수 있다. 예를들어 할당된 티켓의 상태 비율을 계산하는 코드는 다음과 같이 만들 수 있다.

# in lib/helpdesk/support/representative.ex

  calculations do
    # open_tickets, total_tickets 는 aggregates 값
    calculate :percent_open, :float, expr(open_tickets / total_tickets)
  end

iex> Helpdesk.Support.Representative
|> Ash.Query.filter(percent_open > 0.25)
|> Ash.Query.sort(:percent_open)
|> Ash.Query.load(:percent_open)
|> Ash.read!()


19:00:02.609 [debug] QUERY OK source="representatives" db=4.2ms queue=18.6ms idle=1420.5ms
SELECT r0."id", r0."name", (coalesce(s1."open_tickets", $1::bigint)::bigint::decimal / coalesce(s1."total_tickets", $2::bigint)::bigint::decimal)::float FROM "representatives" AS r0 LEFT OUTER JOIN LATERAL (SELECT st0."representative_id" AS "representative_id", coalesce(count(*) FILTER (WHERE st0."status"::varchar = $3::varchar), $4::bigint)::bigint AS "open_tickets", coalesce(count(*) FILTER (WHERE $5), $6::bigint)::bigint AS "total_tickets" FROM "public"."tickets" AS st0 WHERE (r0."id" = st0."representative_id") GROUP BY st0."representative_id") AS s1 ON TRUE WHERE ((coalesce(s1."open_tickets", $7::bigint)::bigint::decimal / coalesce(s1."total_tickets", $8::bigint)::bigint::decimal)::float > $9::float) ORDER BY (coalesce(s1."open_tickets", $10::bigint)::bigint::decimal / coalesce(s1."total_tickets", $11::bigint)::bigint::decimal)::float [0, 0, :open, 0, true, 0, 0, 0, 0.25, 0, 0]
[
  %Helpdesk.Support.Representative{
    id: "f773d41f-24f8-4d38-a5ef-a98a187de034",
    name: "Joe Armstrong",
    total_tickets: #Ash.NotLoaded<:aggregate, field: :total_tickets>,
    open_tickets: #Ash.NotLoaded<:aggregate, field: :open_tickets>,
    closed_tickets: #Ash.NotLoaded<:aggregate, field: :closed_tickets>,
    percent_open: 0.5,
    tickets: #Ash.NotLoaded<:relationship, field: :tickets>,
    __meta__: #Ecto.Schema.Metadata<:loaded, "representatives">
  },
  %Helpdesk.Support.Representative{
    id: "a4467ac7-6f4e-49b1-bdea-9dc98acb5ac9",
    name: "Test User 2",
    total_tickets: #Ash.NotLoaded<:aggregate, field: :total_tickets>,
    open_tickets: #Ash.NotLoaded<:aggregate, field: :open_tickets>,
    closed_tickets: #Ash.NotLoaded<:aggregate, field: :closed_tickets>,
    percent_open: 0.5,
    tickets: #Ash.NotLoaded<:relationship, field: :tickets>,
    __meta__: #Ecto.Schema.Metadata<:loaded, "representatives">
  }
]

집계과 계산을 잘 활용하면 리소스 별 통계성 값 들을 꽤 깔끔하게 코드로 표현할 수 있다. 예제의 코드들은 Ash 가 만들어 주는 쿼리들 이었지만 원한다면 직접 커스텀 집계 쿼리를 만들 수 도 있다. 적절히 사용하면 리소스의 명료한 표현에 큰 도움이 될 것 같다.

댓글 남기기