-
Ecto Composable Query기술 동향 2020. 11. 16. 17:55
medium.com/flatiron-labs/how-to-compose-queries-in-ecto-b71311729dac
How to Compose Queries in Ecto
Composable queries in Ecto including how to safely join through tables.
medium.com
Ecto.Query가 얼마나 Composable한가에 대한 짧은 토픽입니다.(구글 번역)
간단한 쿼리
좋습니다. 간단한 쿼리로 시작하겠습니다. 파란 차를 모두 찾아 봅시다. 이를 위해서는 Ecto.Query.where/3 만 있으면됩니다. 좋은 파이프 기능을 위해 키워드 구문과 달리 표현식 구문을 사용하는 것을 좋아하므로 앞으로 이러한 예를 살펴 보겠습니다. 이제 모든 파란색 자동차를 찾고 싶다고 말했지만이 기능을 좀 더 재사용 가능하게 만들어 모든 색상을 사용할 수 있도록하겠습니다. 이를 위해 다음과 같이 할 수 있습니다.
이제이를 시도해보기 위해 IEX 세션으로 이동하여 EctoCars.Car.with_color (“blue”) |> EctoCars.Repo.all ()을 실행하여이 쿼리를 시도해 보겠습니다.
def with_color(color) do Car |> where([c], c.color == ^color) end
조인으로 쿼리
def with_transmission(type) do Car |> join(:left, [c], s in Specification, on: c.specification_id == s.id) |> join(:left, [c, s], t in Transmission, on: s.transmission_id == t.id) |> where([c, s, t], t.type == ^type) end
이 작업을 수행하려면 실제로 두 번 query해야했습니다. specification을 통과 한 다음 다시 전송합니다. 우리는 Transmission type을 인수로 받아들이고 마지막에 where 절에서 이를 사용하여이 쿼리 함수를 다시 사용할 수 있도록했습니다.
주의해야 할 한 가지는 결합에 대한 세 번째 인수로 사용하는 문자 목록이 바인딩 된 변수 일 뿐이라는 것입니다. 예를 들어 두 번째 조인에서는 [c, s] 사용에 특별한 것이 없습니다. 이러한 문자는 이전 함수 호출에서 사용 된 것과 동일한 문자 였기 때문에 우리에게 의미가 있습니다. 두 번째 조인의 s를 다른 문자로 변경하면 쿼리가 전혀 변경되지 않습니다. (역자주1: 단, 순서는 중요합니다.!! [c, s] -> [a, b] 로 해도 상관 없습니다만 첫 번째가 Car이고 두 번째가 Spec. 이라는 것은 기억해야 합니다.)
콘솔에서 이것을 시도하면 다음을 볼 수 있습니다.
Composable 하게 만들기
이제 자동 변속기가있는 파란색의 모든 차를 정말로 찾고 있다면 어떨까요? 음, 완전히 새로운 쿼리를 작성하는 대신 기존의 두 쿼리 만 재사용 할 수 있다면 좋을 것입니다. 이렇게 하려면 유형 / 색상 인수뿐만 아니라 추가 할 기존 쿼리를 가져 오기 위해 함수를 약간 리팩터링해야 합니다.
첫 번째 인수를 선택 사항으로 설정하여 빌드중인 쿼리를 전달하도록 선택하거나 cars 테이블에서 쿼리를 기본적으로 수행하도록 선택할 수 있습니다. 이제
EctoCars.Car |> EctoCars.Car.with_color("blue") |> EctoCars.Car.with_transmission("automatic") |> EctoCars.Repo.all
하나의 긴 Ecto 쿼리를 작성하는 것처럼이를 연결할 수 있습니다.
여러 조인으로 작성
따라서 이전에했던 작업과 함께 엔진에 대한 쿼리 함수를 만들어 보겠습니다.
def with_engine_horse_power(query \\ Car, horse_power) do query |> join(:left, [c], s in Specification, on: c.specification_id == s.id) |> join(:left, [c, s], e in Engine, on: s.engine_id == e.id) |> where([c, s, e], e.horse_power > ^horse_power) end
iex(10)> EctoCars.Car |> EctoCars.Car.with_color("blue") |> EctoCars.Car.with_transmission("automatic") |> EctoCars.Car.with_engine_horse_power(200) |> EctoCars.Repo.all() ** (Ecto.QueryError) lib/ecto_cars/cars/car.ex:37: field `horse_power` in `where` does not exist in schema EctoCars.Transmission in query: from c0 in EctoCars.Car, left_join: s1 in EctoCars.Specification, on: c0.specification_id == s1.id, left_join: t2 in EctoCars.Transmission, on: s1.transmission_id == t2.id, left_join: s3 in EctoCars.Specification, on: c0.specification_id == s3.id, left_join: e4 in EctoCars.Engine, on: s1.engine_id == e4.id, where: c0.color == ^"blue", where: t2.type == ^"automatic", where: t2.horse_power > ^200, select: c0 (elixir) lib/enum.ex:1925: Enum."-reduce/3-lists^foldl/2-0-"/3 (elixir) lib/enum.ex:1418: Enum."-map_reduce/3-lists^mapfoldl/2-0-"/3 (elixir) lib/enum.ex:1418: Enum."-map_reduce/3-lists^mapfoldl/2-0-"/3
여기서 일어난 일을 분석 할 수 있는지 보겠습니다. 오류를 살펴보면 Transmission 스키마에 horse_power가 존재하지 않는다는 의미입니다. 이를 좀 더 잘 이해하기 위해 이러한 모든 쿼리 조각을 풀고 함수 외부에서 함께 작성해 보겠습니다.
Car |> where([c], c.color == ^color) |> join(:left, [c], s in Specification, on: c.specification_id == s.id) |> join(:left, [c, s], t in Transmission, on: s.transmission_id == t.id) |> where([c, s, t], t.type == ^type) |> join(:left, [c], s in Specification, on: c.specification_id == s.id) |> join(:left, [c, s], e in Engine, on: s.engine_id == e.id) |> where([c, s, e], e.horse_power > ^horse_power)
여기서는 좀 더 쉽게 볼 수 있습니다. 결합이 join과 함께 작동하는 기본 방식으로 인해 참조하는 테이블은 해당 목록에서의 위치와 전적으로 관련됩니다. 따라서 8 행에서 engines 테이블에 대해 쿼리 할 때 실제로이 쿼리의 세 번째 테이블에 대해 쿼리합니다. [Trasnmission!!!] 이것을있는 그대로 작동 시키려면 다음을 수행해야합니다.
Car |> where([c], c.color == ^color) |> join(:left, [c], s in Specification, on: c.specification_id == s.id) |> join(:left, [c, s], t in Transmission, on: s.transmission_id == t.id) |> where([c, s, t], t.type == ^type) |> join(:left, [c, s, t], e in Engine, on: s.engine_id == e.id) |> where([c, s, t, e], e.horse_power > ^horse_power)
우선 사양에 대한 이중 조인을 제거했습니다. 실제로 누가 그것을 필요로하기 때문입니다. 또한 바인딩 목록이 Transmission에 대한 기존 바인딩을 인식하도록 엔진에 대한 조인 및 쿼리를 업데이트했습니다. 현재이 쿼리는 이전에 수행 된 쿼리 (사양에 따라)와이 쿼리를 호출하기 전에 동일한 수의 테이블이 쿼리에 조인되었는지에 따라 달라집니다. 동작은 하겠지만 만족스럽지 않습니다. 여기에 더 나은 방법이 있어야합니다.
Named Binding
Named Binding을 사용하려면 join을 호출 할 때 as 옵션을 전달합니다. join (query, : left, [c], s in Specification, as : : specifications, on : c.specification_id == s.id). 그런 다음 다음 키 사양을 사용하여 쿼리에서 사양 변수를 바인딩 할 수 있습니다. : join (query, : left, [c, specification : s], t in Transmission, as : : transmissions, on : s.transmission_id == t.id). 이제 명Named Binding을 사용하도록 함수를 다시 작성해 보겠습니다. (역자주2: as 옵션은 딱 2 군데서 사용합니다. join & from)
def with_transmission(query \\ Car, type) do query |> join(:left, [c], s in Specification, as: :specifications, on: c.specification_id == s.id) |> join(:left, [c, specifications: s], t in Transmission, as: :transmissions, on: s.transmission_id == t.id) |> where([c, transmissions: t], t.type == ^type) end def with_engine_horse_power(query \\ Car, horse_power) do query |> join(:left, [c], s in Specification, as: :specifications, on: c.specification_id == s.id) |> join(:left, [c, specifications: s], e in Engine, as: :engines, on: s.engine_id == e.id) |> where([c, engines: e], e.horse_power > ^horse_power) end
하지만 다시 연결하려고하면 흥미로운 일이 발생합니다.
iex(16)> EctoCars.Car |> EctoCars.Car.with_color("blue") |> EctoCars.Car.with_transmission("automatic") |> EctoCars.Car.with_engine_horse_power(200) |> EctoCars.Repo.all() ** (Ecto.Query.CompileError) alias `:specifications` already exists (ecto_cars) lib/ecto_cars/cars/car.ex:35: EctoCars.Car.with_engine_horse_power/2
동일한 키를 다시 바인딩 할 수 없습니다. 간단한 해결책은 with_engine_horse_power 함수에서 사양에 대한 조인을 제거하는 것이지만, 사양에 조인하는 함수가 호출 된 후에 만이 함수를 호출해야하는 위치에 다시 갇혀 있습니다.
여기에서 가장 좋은 방법은 쿼리가 이미 특정 테이블에 조인되었는지 확인하고 확인하지 않은 경우에만 다시 조인하는 것입니다. has_named_binding?으로 깔끔하게이 작업을 수행 할 수 있습니다. 이 함수를 사용하면 쿼리에 이미 해당 바인딩이 있는지 확인하고 확인할 수 있습니다.
def with_transmission(query \\ Car, type) do query |> join_specifications() |> join(:left, [c, specifications: s], t in Transmission, as: :transmissions, on: s.transmission_id == t.id) |> where([c, transmissions: t], t.type == ^type) end def with_engine_horse_power(query \\ Car, horse_power) do query |> join_specifications() |> join(:left, [c, specifications: s], e in Engine, as: :engines, on: s.engine_id == e.id) |> where([c, engines: e], e.horse_power > ^horse_power) end defp join_specifications(query) do if has_named_binding?(query, :specifications) do query else query |> join(:left, [c], s in Specification, as: :specifications, on: c.specification_id == s.id) end end
join_specifications 도우미 함수는 조인을 확인하고 필요한 경우 쿼리에 추가하는 작업을 담당합니다. 이제 마지막으로 특정 순서에 의존하지 않거나 두 번째를 호출하기 전에 항상 호출해야하는 하나의 함수에 의존하는 구성 가능한 쿼리를 작성할 수 있습니다!
iex(17)> EctoCars.Car |> EctoCars.Car.with_color("blue") |> EctoCars.Car.with_transmission("automatic") |> EctoCars.Car.with_engine_horse_power(200) |> EctoCars.Repo.all() 17:31:25.303 [debug] QUERY OK source="cars" db=2.6ms queue=3.8ms SELECT c0."id", c0."color", c0."vin_number", c0."specification_id" FROM "cars" AS c0 LEFT OUTER JOIN "specifications" AS s1 ON c0."specification_id" = s1."id" LEFT OUTER JOIN "transmissions" AS t2 ON s1."transmission_id" = t2."id" LEFT OUTER JOIN "engines" AS e3 ON s1."engine_id" = e3."id" WHERE (c0."color" = $1) AND (t2."type" = $2) AND (e3."horse_power" > $3) ["blue", "automatic", 200] [ %EctoCars.Car{ __meta__: #Ecto.Schema.Metadata<:loaded, "cars">, color: "blue", id: 3, specification: #Ecto.Association.NotLoaded<association :specification is not loaded>, specification_id: 1, vin_number: "my_dream_car" } ]
'기술 동향' 카테고리의 다른 글
Elixir Process (0) 2021.06.28 Process Register (0) 2021.04.05 Ecto.Multi (0) 2020.10.06 History of Erlang (0) 2020.06.25 Raft Consensus Algorithm (0) 2020.06.25