使用Ecto正确设置检查约束

时间:2019-07-10 13:26:26

标签: elixir ecto

我的模型中有这个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 个答案:

答案 0 :(得分:1)

引用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/4validate_subset/4之类的函数。您甚至可以使用validate_change/4编写自己的检查器(如果需要更多说明,请告诉我)。如果使用这些验证器,则更改集将在调用数据库之前起作用。

答案 1 :(得分:0)

my answer to your previous question,中,如果在为插入创建变更集时添加一些输出:

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返回了无效的更改集。

那是因为检查约束是db规则-不是验证程序。 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)实际执行插入操作,您才能知道插入变更集是否会成功。