Ecto의 "or_where"함정을 주의하십시오.

18886 단어 elixirecto
이 게시물에서는 Ecto의 or_where 기능을 사용할 때 발생할 수 있는 오류를 강조하고 싶습니다. 이것은 Ecto의 결함이 아닙니다. 단지 이 함수가 Ecto.Query 함수와 함께 작동하는 방식에 대해 잘못된 결론에 도달할 수 있는 방법일 뿐입니다.

때때로 나는 특정 리소스를 필터링하기 위해 양식을 작성하는 자신을 발견합니다. 이 양식을 사용하면 특정 속성이 특정 값과 같은 레코드( select 생각) 또는 특정 속성이 in 값 목록( multi-select 생각)인 레코드를 필터링할 수 있습니다.

예를 들어, 정제인 모든 약을 반환하고 메스꺼움이나 두통을 치료합니다. 즉, 태블릿이 아닌 레코드는 반환되지 않아야 합니다. 그리고 그들이 치료할 수 있는 조건 목록에 "메스꺼움"또는 "두통"이 없는 정제는 반환되지 않아야 합니다. 모든 쿼리에 AND 절을 추가하고 이것이 어떻게 중단되는지 강조하기 위해 부울 필드 available 도 추가합니다. 모든 쿼리에 대해 사용할 수 없는 레코드를 필터링하는 절을 포함합니다.

다음은 스키마의 관련 부분입니다.

schema "medicines" do
  ...
  field :type, :string
  field :conditions, {:array, :string}
  field :available, :boolean, default: false
end


첫 번째 부분은 쉽습니다. typetablet인 레코드를 반환합니다.

defmodule MedicineCupboard.MedicineFilter do
  import Ecto.Query

  def type_filter(query, %{"type" => type}) do
    where(query, [m], m.type == ^type)
  end
  def type_filter(query, _no_type), do: query
end


available 절부터 시작하여 다음 SQL을 얻습니다.

SELECT m0."id", m0."name", m0."type", m0."conditions", m0."available" 
FROM "medicines" AS m0 WHERE 
(m0."available" = TRUE) AND (m0."type" = $1) ["tablet"]


두 번째 필터는 조금 더 복잡합니다. 조건 필터로 전달된 값 중 하나가 conditions 문자열 배열에 있는 의약품을 반환하려고 합니다.

이 두 번째 필터를 아래와 같이 작성하는 것이 좋습니다.

defmodule MedicineCupboard.MedicineFilter do
  ...

  # This does not build the intended query!
  def condition_filter(query, %{"condition" => conditions_list}) do
    conditions_list
    |> Enum.reduce(query, fn condition, query -> 
      or_where(query, [m], ^condition in m.conditions)
    end)
  end
end


이 쿼리의 문제점은 빌드하는 SQL이 다음과 같다는 것입니다.

SELECT m0."id", m0."name", m0."type", m0."conditions", m0."available" 
FROM "medicines" AS m0 WHERE 
((m0."available" = TRUE)) 
OR ($1 = ANY(m0."conditions")) 
OR ($2 = ANY(m0."conditions")) ["nausea", "headache"]


Ecto 쿼리는 다음과 같습니다.

#Ecto.Query<from m0 in MedicineCupboard.Medicine, where: m0.available == true,
 or_where: ^"nausea" in m0.conditions, or_where: ^"headache" in m0.conditions>


사용 가능 여부에 관계없이 조건 필드에 두통이 포함된 모든 약은 반환됩니다. 가용성에 대한 조항은 조건에 대한 조항과 함께 OR ed입니다.

우리가 원하는 것은 available = true AND nausea in conditions, or headache in conditions 입니다.

Ecto.Query or_where/3 함수는 이전의 모든 조건에 OR 절을 추가하여 효과적으로 다음과 같은 결과를 얻습니다.
지금까지의 모든 조건을 충족하거나 이 새로운 or_where 절만 충족하십시오.

이 문제를 해결한 방법은 subquery을 사용하는 것입니다.

defmodule MedicineCupboard.MedicineFilter do
  import Ecto.Query

  def type_filter(query, %{"type" => type}) do
    where(query, [m], m.type == ^type)
  end
  def type_filter(query, _no_type), do: query

  def condition_filter(query, %{"condition" => condition_list}) do
    conditions_query = condition_list
    |> Enum.reduce(Medicine, fn condition, query -> 
      or_where(query, [m], ^condition in m.conditions)
    end)
    |> select([:id])

    where(query, [m], m.id in subquery(conditions_query))
  end
end


이것은 하나의 하위 쿼리에서 다양한 or_where 절을 그룹화한 다음 들어오는 쿼리에 AND where 절을 추가합니다. 여기에서 조건 하위 쿼리에서 ID가 반환되는 레코드만 반환합니다.

결과 SQL은 다음과 같습니다.

SELECT m0."id", m0."name", m0."type", m0."conditions", 
m0."available" FROM "medicines" AS m0 WHERE 
(m0."available" = TRUE) 
AND 
(m0."id" IN 
  (SELECT sm0."id" FROM "medicines" AS sm0 WHERE 
    ($1 = ANY(sm0."conditions")) OR ($2 = ANY(sm0."conditions"))
  )
) 
["nausea", "headache"]


Ecto 쿼리는 다음과 같습니다.

#Ecto.Query<from m0 in MedicineCupboard.Medicine, where: m0.available == true,
 where: m0.id in subquery(#Ecto.Query<from m0 in MedicineCupboard.Medicine, or_where: ^"nausea" in m0.conditions, or_where: ^"headache" in m0.conditions, select: [:id]>)>


마지막으로 이제 유형 및 조건 필터를 결합하고 사용 가능 및 유형 필터가 AND 과 결합되고 OR 절이 포함된 조건 하위 쿼리도 AND 을 사용하여 기존 쿼리에 추가되는지 확인할 수 있습니다.

params = %{"type" => "tablet", "conditions" => ["nausea", "headache"]}
Medicine
|> where([m], m.available == true)
|> MedicineFilter.type_filter(params)
|> MedicineFilter.condition_filter(params)
|> Repo.all()


그러면 다음과 같은 외부 쿼리가 제공됩니다.

#Ecto.Query<from m0 in MedicineCupboard.Medicine, where: m0.available == true,
 where: m0.type == ^"tablet",
 where: m0.id in subquery(#Ecto.Query<from m0 in MedicineCupboard.Medicine, or_where: ^"nausea" in m0.conditions, or_where: ^"headache" in m0.conditions, select: [:id]>)>


그리고 SQL:

SELECT m0."id", m0."name", m0."type", m0."conditions", 
m0."available" FROM "medicines" AS m0 
WHERE (m0."available" = TRUE) 
AND (m0."type" = $1) 
AND (m0."id" IN (
  SELECT sm0."id" FROM "medicines" AS sm0 WHERE 
    ($2 = ANY(sm0."conditions")) OR ($3 = ANY(sm0."conditions"))
  )
) ["tablet", "fever", "headache"]


누군가에게 도움이 되기를 바라며 개선 사항에 대한 의견이 있으면 언제든지 공유해 주세요.

코드를 가지고 놀고 싶다면 여기 github의 샘플 프로젝트: https://github.com/Ivor/medicine_cupboard

좋은 웹페이지 즐겨찾기