Правильная настройка проверочного ограничения с помощью Ecto - PullRequest
2 голосов
/ 10 июля 2019

У меня есть эта check_constraint в моей модели.

  def changeset(struct, params \\ %{}) do
    struct
    |> cast(params, @all_fields)
    |> validate_required(@required_fields)
    |> check_constraint(:stars, name: :stars_range, message: "stars must be between 1 and 5")
  end

Ограничение создания успешно перенесено.

create constraint("reviews", "stars_range", check: "stars>=1 and stars<=5")

Но когда я запускаю этот тест, набор изменений действителен? Я ожидаю, что он будет недействительным, потому что я передаю целое число 7 в столбец stars. который имеет ограничение 1 through 5. Кто-нибудь знает, что здесь не так?

test "requires stars to be within range of 1-5" do
    user = insert(:user)
    project = insert(:project, owner: user)
    user_project_map = %{project_id: project.id, user_id: user.id}
    review_map = Map.merge(@valid_attrs, user_project_map)

    attrs = %{review_map | stars: 7}
    changeset = Review.changeset(%Review{}, attrs)
    refute changeset.valid?
  end

Ответы [ 2 ]

1 голос
/ 10 июля 2019

Цитата из docs :

(...) Теперь при вызове Repo.insert / 2 или Repo.update / 2, если цена не является положительной,оно будет преобразовано в ошибку и {: error, changeset} будет возвращено хранилищем.Обратите внимание, что ошибка произойдет только после попадания в базу данных, поэтому она не будет видна до тех пор, пока не пройдут все другие проверки.

Это означает, что check_constraint происходит, только если запрос попадает в базу данных.Следовательно, ваш changeset.valid? возвращает true, когда вы проверяете проверку перед фактическим вызовом базы данных.Созданное вами ограничение создается внутри базы данных, поэтому у Ecto нет возможности узнать, что именно проверяет это ограничение, прежде чем вызывать его.Обычно такие ограничения используются для более сложных проверок или если у вас уже есть ограничения, определенные в вашей базе данных (возможно, потому что вы перенесли базу данных из другой системы?).Если вы хотите увидеть свое ограничение в действии, вам нужно просто написать в своем тесте:

attrs = %{review_map | stars: 7}
changeset = Review.changeset(attrs)
{:error, changeset} = Repo.insert(changeset)
refute changeset.valid?

Если вам требуется Changeset для проверки некоторых условий перед вызовом базы данных, вам следует использовать такие функции, как validate_inclusion/4или validate_subset/4.Вы даже можете написать свою собственную программу проверки, используя validate_change/4 (дайте мне знать, если вам нужно больше объяснений, как это сделать).Если вы используете эти валидаторы, то ваша ревизия будет работать до вызова базы данных.

0 голосов
/ 10 июля 2019

В мой ответ на ваш предыдущий вопрос, , если я добавлю какой-нибудь вывод при создании набора изменений для вставки:

defmodule Foo do
  alias Foo.Review
  require Logger

  @repo Foo.Repo

  def list_reviews do
    @repo.all(Review)
  end

  def insert_review(attrs) do
    changeset = Review.changeset(%Review{}, attrs)

    ##   HERE ###
    Logger.debug("changeset.valid? => #{changeset.valid?}")

    @repo.insert(changeset)
  end

  def delete_book(%Book{}=book) do
    @repo.delete(book)
  end

end

вот вывод в iex:

ex(3)> reviews = Foo.list_reviews                         
[debug] QUERY OK source="reviews" db=3.4ms
SELECT r0."id", r0."title", r0."contents", r0."stars", r0."inserted_at", r0."updated_at" FROM "reviews" AS r0 []
[]


## VALID DATA ###

iex(4)> Foo.insert_review(%{title: "book", contents: "good", stars: 4})  
[debug] changeset.valid? => true
[debug] QUERY OK db=2.3ms queue=2.0ms
INSERT INTO "reviews" ("contents","stars","title","inserted_at","updated_at") VALUES ($1,$2,$3,$4,$5) RETURNING "id" ["good", 4, "book", ~N[2019-07-10 17:23:06], ~N[2019-07-10 17:23:06]]
{:ok,
 %Foo.Review{
   __meta__: #Ecto.Schema.Metadata<:loaded, "reviews">,
   contents: "good",
   id: 4,
   inserted_at: ~N[2019-07-10 17:23:06],
   stars: 4,
   title: "book",
   updated_at: ~N[2019-07-10 17:23:06]
 }}


## INVALID DATA ##

iex(5)> Foo.insert_review(%{title: "movie", contents: "shite", stars: 0})
[debug] changeset.valid? => true
[debug] QUERY ERROR db=6.1ms queue=1.5ms
INSERT INTO "reviews" ("contents","stars","title","inserted_at","updated_at") VALUES ($1,$2,$3,$4,$5) RETURNING "id" ["shite", 0, "movie", ~N[2019-07-10 17:23:16], ~N[2019-07-10 17:23:16]]
{:error,
 #Ecto.Changeset<
   action: :insert,
   changes: %{contents: "shite", stars: 0, title: "movie"},
   errors: [
     stars: {"stars must be between 1 and 5 (inclusive)",
      [constraint: :check, constraint_name: "stars_range"]}
   ],
   data: #Foo.Review<>, 
   valid?: false
 >}

Для недопустимых данных вы можете видеть, что набор изменений действителен до вызова @repo.insert(changeset), а затем после сбоя вставки Ecto возвращает неверный набор изменений.

Это потому, что проверочное ограничение является правилом БД, а не валидатором. Функция changeset () применяет все указанные вами валидаторы и, таким образом, определяет, действителен ли набор изменений. Если набор изменений действителен, то Ecto фактически пытается выполнить вставку в БД. В этот момент БД выполняет проверочное ограничение, чтобы определить, будет ли вставка успешной или нет. Если проверочное ограничение не выполняется, БД выдает ошибку. Ecto ловит эту ошибку, а затем добавляет сообщение, указанное здесь:

   |> check_constraint(
        :stars,
        name: :stars_range,
        message: "stars must be between 1 and 5 (inclusive)"
      )

для ошибок в наборе изменений, устанавливает changeset.valid? в false, затем возвращает {:error, changeset}.

Существует разница в выходных данных, когда валидатор терпит неудачу, v. Когда проверочное ограничение не выполняется. Если я изменю проверки на:

  def changeset(%Foo.Review{}=review, attrs \\ %{}) do
    review
    |> cast(attrs, [:title, :contents, :stars])
    |> validate_required(:title)  ##<==== ADDED THIS VALIDATION
    |> check_constraint(
        :stars,
        name: :stars_range,
        message: "stars must be between 1 and 5 (inclusive)"
      )
  end

затем попробуйте сделать вставку без заголовка, вот вывод:

iex(6)> Foo.insert_review(%{contents: "crowded", stars: 1})
[debug] changeset.valid? => false
{:error,
 #Ecto.Changeset<
   action: :insert,
   changes: %{contents: "crowded", stars: 1},
   errors: [title: {"can't be blank", [validation: :required]}],
   data: #Foo.Review<>,
   valid?: false
 >}

Сравнить с:

## INVALID DATA ##

iex(5)> Foo.insert_review(%{title: "movie", contents: "shite", stars: 0})
[debug] changeset.valid? => true
[debug] QUERY ERROR db=6.1ms queue=1.5ms
INSERT INTO "reviews" ("contents","stars","title","inserted_at","updated_at") VALUES ($1,$2,$3,$4,$5) RETURNING "id" ["shite", 0, "movie", ~N[2019-07-10 17:23:16], ~N[2019-07-10 17:23:16]]
{:error,
 #Ecto.Changeset<
   action: :insert,
   changes: %{contents: "shite", stars: 0, title: "movie"},
   errors: [
     stars: {"stars must be between 1 and 5 (inclusive)",
      [constraint: :check, constraint_name: "stars_range"]}
   ],
   data: #Foo.Review<>, 
   valid?: false
 >}

В последнем выводе обратите внимание:

 [debug] QUERY ERROR db=6.1ms queue=1.5ms

Разница в выходных данных указывает на то, что только после всех проверок Ecto пытается выполнить вставку. Когда вставка фактически выполняется, база данных применяет проверочное ограничение, которое вызывает сбой вставки, и ecto записывает QUERY ERROR.

Суть в следующем: только то, что набор изменений действителен, не означает, что вставка будет успешной. Если функция changeset() добавляет constraints в базу данных, то вы не можете знать, будет ли вставка набора изменений успешной, пока вы фактически не выполните вставку, вызвав @repo.insert(changeset).

...