【问题标题】:With Ecto, validate that a changeset with 2 different related models have the same parent model使用 Ecto,验证具有 2 个不同相关模型的变更集是否具有相同的父模型
【发布时间】:2019-11-16 08:58:09
【问题描述】:

在我的应用程序中,我有一种方法可以创建一个新的responseresponseplayermatch 都有 belongs_to 关系。

此外,playermatch 都与 team 具有 belongs_to 关系。

看起来像这样:

当插入新的response 时,我想验证变更集中具有player_idmatch_id 外键的playermatch 属于同一个team

目前我正在实现这一点,如下所示。首先,定义一个自定义验证来检查属于外键的记录:

def validate_match_player(changeset) do
  player_team =
    Player
    |> Repo.get(get_field(changeset, :player_id))
    |> Map.get(:team_id)

  match_team =
    Match
    |> Repo.get(get_field(changeset, :match_id))
    |> Map.get(:team_id)

  cond do
    match_team == player_team -> changeset
    true -> changeset |> add_error(:player, "does not belong to the same team as the match")
  end
end

并将验证用作变更集的一部分:

def changeset(model, params \\ %{}) do
  model
  |> cast(params, [:player_id, :match_id, :message])
  |> validate_required([:player_id, :match_id, :message])
  |> foreign_key_constraint(:match_id)
  |> foreign_key_constraint(:player_id)
  |> validate_match_player()
  |> unique_constraint(
    :player,
    name: :responses_player_id_match_id_unique,
    message: "already has an response for this match"
  )
end

这很好用,但需要一些额外的 SQL 查询来查找相关记录,以便获取它们的 team_id 外键来比较它们。

有没有更好的方法来做到这一点,也许使用约束来避免额外的查询?

【问题讨论】:

  • AFAIK,约束不能访问其他表的数据。因此,要么在 SQL 中编写一个触发器,在不满足该条件时阻止编辑,要么像这样保留它。

标签: validation elixir ecto changeset


【解决方案1】:

我有两个可能的改进:

  • 应用程序级解决方案:您只需查询一次,而不是两次查询。
  • 数据库级解决方案:您为数据库中的检查创建触发器。

应用级解决方案

现在您有两个查询来检查该球员和比赛是否属于同一支球队。这意味着两次往返数据库。如果您只使用一个查询,例如,您可以将其减少一半。给出以下查询:

    SELECT COUNT(*)
      FROM players AS p
INNER JOIN matches AS m
        ON p.team_id = m.team_id
     WHERE p.id = NEW.player_id AND m.id = NEW.match_id

你会改变你的功能如下:

def validate_match_player(changeset) do
  player_id = get_field(changeset, :player_id)
  match_id = get_field(changeset, :match_id)

  [result] =
    Player
    |> join(:inner, [p], m in Match, on: p.team_id == m.team_id)
    |> where([p, m], p.id == ^player_id and m.id == ^match_id)
    |> select([p, m], %{count: count(p.id)})
    |> Repo.all()

  case result do
    %{count: 0} ->
      add_error(changeset, :player, "does not belong to the same team as the match")
    _ ->
      changeset
  end
end

数据库级解决方案

我假设您使用的是 PostgreSQL,所以我的回答将与您在 PostgreSQL 手册中找到的内容相对应。

没有(干净的)方法可以在执行此操作的表中定义约束。约束只能访问定义它们的表。某些约束只能从它们定义的内容中访问列,仅此而已 (CHECK CONSTRAINT)。

最好的方法是编写一个触发器来验证这两个字段,例如:

CREATE OR REPLACE FUNCTION trigger_validate_match_player()
  RETURNS TRIGGER AS $$
    IF (
         SELECT COUNT(*)
         FROM players AS p
         INNER JOIN matches AS m
         ON p.team_id = m.team_id
         WHERE p.id = NEW.player_id AND m.id = NEW.match_id
       ) = 0
    THEN
      RAISE 'does not belong to the same team as the match'
        USING ERRCODE 'invalid_match_player';
    END IF;

    RETURN NEW;
  $$ LANGUAGE plpgsql;

CREATE TRIGGER responses_validate_match_player
  BEFORE INSERT OR UPDATE ON responses
  FOR EACH ROW
  EXECUTE PROCEDURE trigger_validate_match_player();

前一个触发器在失败时会引发异常。这也意味着 Ecto 将引发异常。你可以看到如何处理这个异常here

最后,维护触发器并不容易,除非您使用 sqitch 之类的东西进行数据库迁移。

PS:如果您好奇,在CHECK 约束中执行此操作的非常脏方法是定义一个基本上绕过限制的 PostgreSQL 函数。我不会推荐它。

我希望这会有所帮助:)

【讨论】:

  • 很好的答案,谢谢!我认为您关于难以维护触发器的观点是有效的。我确实考虑过一个带有连接的查询,这看起来像是要走的路。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-08-05
  • 1970-01-01
  • 1970-01-01
  • 2012-08-27
  • 2016-06-18
相关资源
最近更新 更多