Usage of Ecto Multi (1)
Elixir로 서버개발 시작한지 한달.. 요즘은 Ecto와 씨름하는중이다.
Usage of Ecto Multi (1)
웹 요청이 들어왔을 때 한 트랜잭션 안에 수행되어야 할 것들이 있다. 예를 들어 Post와 Comment가 one-to-many 관계로 있다면 Post를 삭제할 때 연관된 Comment들도 같이 삭제되는 식이다. 예시는 간단하지만 실제 비즈니스 로직은 꽤 복잡한 경우가 허다하다.
요즘 주로 고민하는건 트랜잭션 처리에서 조건이 들어가는 경우이다. 예를 들어 User가 Post들을 가지고 있다고 했을 때 User 스키마에 대표 Post의 title 필드를 캐싱하는 display_post 필드란 녀석이 들어가 있다. 근데 그 Post을 변경하는 컨텍스트 함수가 있다. 이 때 그냥 post의 description을 바꾸는건 update하나면 충분하고, title 컬럼을 바꾸면 연관된 User를 찾아 display_post 필드를 변경해주는게 한 트랜잭션으로 묶여야 한다. 나는 일단 먼저 다음과 같이 풀었다.
defmodule Test.MyContext do
# .. some ceremony code
def update_post(post, params) do
uc = Post.update_changeset(post, params)
Multi.new()
|> Multi.update(:update_post, uc, post.author_id)
|> update_related_user(uc, user)
# ...
|> Repo.transaction()
|> case do
{:ok, %{update_post: post}} -> {:ok, post}
{:error, _, _, _} -> {:error, "fail to update post"}
end
end
# title이 변경되는 changeset이라면
defp update_related_user(multi, Ecto.Changeset{changes: %{title: title}} = changeset, user_id) do
user = Repo.get(User, user_id)
user_changeset = User.update_changeset(user, %{display_post: title})
Multi.update(:update_user, user_changeset)
end
# 그 외엔 그냥 그대로 반환
defp update_related_user(multi, _, _), do: multi
end
Multi.t()
를 받아 Multi.t()
를 반환하는 내부 함수를 만들어서, changeset을 인자로 받아서 changeset의 변경사항 changes 에 title이 포함되어 있으면 추가 multi를 수행하고, 아니라면 그대로 반환하는 것이다. params를 받지않고 굳이 changeset을 받은 이유는 title이 변경될 경우에만 user update를 수행하기 위해서다.
현재 내 수준에서 그렇게 나쁘게 보이진 않지만 못내 불만족스럽다. changeset을 불필요하게 건네주는것같기도 하고 defp 함수들이 그렇게 깔끔하게 잘 나오지 않는 느낌이다. 이게 괜찮은 방법인지 확신이 서질 않는다. 근데 하긴 요즘 짜는 엘릭서 코드들은 다 그런 생각이 들긴 한다. 본격적으로 엘릭서 코드짠지는 두달도 안됐으니..
다르게 풀면 어떻게 풀수 있을까? 다른 방식을 생각했던건 multi를 계속 받아서 넘기는게 아니라 multi자체를 만들어서 결합하는 방식이다. Ecto.Multi에는 append/2
, prepend/2
, merge/2
같은 함수들이 있다. 만들어진 multi들을 손쉽게 결합 가능하다.
대충 다음과 같지 않을까 싶다.
def update_complex_function(struct, params) do
changeset = update_changeset(struct, params)
update_post_multi = update_post_multi(changeset)
user_update_multi = update_user_by_post_multi(changeset)
do_something = do_something_multi()
# ...
Multi.new()
|> Multi.append(update_a_multi)
|> Multi.append(some_extra_update)
|> Multi.append(do_something)
|> Repo.transaction()
end
defp update_post_multi(changeset) do
Multi.new()
|> Multi.update(:update_post, changeset)
end
좀더 깔끔한거 같기도? 이제 조건문을 어떻게 넣을것인가.. 똑같은 고민이 발생했다. 결국 내부 함수로 풀어줘야 할거같다.
defp update_user_by_post_multi(Ecto.Changeset{data: %Post{}, changes: %{title: title}} = changeset) do
user_changeset = #...
Multi.new()
|> Multi.update(:update_user, user_changeset)
end
defp update_user_by_post_multi(_), do: Multi.new()
굳이 multi를 반환할 필요 없을지도, 그냥 nil을 반환하고 걸러줘도 될것같다. 어차피 Enum.reduce를 사용할 것이다. update_complex_function을 고쳐보자
def update_complex_function(struct, params) do
changeset = update_changeset(struct, params)
update_post_multi = update_post_multi(changeset)
user_update_multi = update_user_by_post_multi(changeset)
do_something_multi = do_something_multi()
# ...
# 위 함수들이 nil을 반환하게 한다면 Enum.reject로 먼저 걸러줘도 될듯
Enum.reduce([
update_post_multi,
user_update_multi,
do_something_multi
], Multi.new(), &Multi.append(&2, &1))
|> Repo.transaction()
end
뭐 역시 나쁘지는 않는데, 가독성, 방어적인 코드 측면에서 이 방법이 맞는지는 역시 아직도 경험 부족이다. 그냥 다양한 방법이 있고 지금은 시행착오를 겪는 시기인듯 싶다. 지금 비즈니스 로직은 그냥 첫번째 방법을 주로 써서 만들었는데, 빨리 좀 깔끔하고 마음에 드는 방법을 찾을 수 있으면 좋겠다.