使用 Ecto,验证具有 2 个不同相关模型的变更集是否具有相同的父模型
With Ecto, validate that a changeset with 2 different related models have the same parent model
在我的应用程序中,我有一个创建新 response
的方法。 response
与 player
和 match
都有 belongs_to
关系。
此外 player
和 match
都与 team
.
有 belongs_to
关系
看起来像这样:
插入新的 response
时,我想验证变更集中具有 player_id
和 match_id
外键的 player
和 match
属于同样 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
外键来比较它们。
是否有更好的方法来避免额外的查询?
我有两个可能的改进:
- 应用层解决方案:查询一次,而不是两次查询。
- 数据库级解决方案:你创建一个触发器来检查数据库。
应用级解决方案
现在你有两个查询来检查球员和比赛是否属于同一个球队。这意味着两次往返数据库。如果你只使用一个查询,你可以减少一半,例如给定以下查询:
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 手册中找到的内容相对应。
没有(干净的)方法可以在 table 中定义约束来执行此操作。约束只能访问定义它们的 table 。某些约束只能从它们定义的内容访问列,仅此而已 (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: If you're curious, the very dirty way of doing this in a CHECK
constraint is by defining a PostgreSQL function that basically bypasses the limitation. I wouldn't recommend it.
希望对您有所帮助:)
在我的应用程序中,我有一个创建新 response
的方法。 response
与 player
和 match
都有 belongs_to
关系。
此外 player
和 match
都与 team
.
belongs_to
关系
看起来像这样:
插入新的 response
时,我想验证变更集中具有 player_id
和 match_id
外键的 player
和 match
属于同样 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
外键来比较它们。
是否有更好的方法来避免额外的查询?
我有两个可能的改进:
- 应用层解决方案:查询一次,而不是两次查询。
- 数据库级解决方案:你创建一个触发器来检查数据库。
应用级解决方案
现在你有两个查询来检查球员和比赛是否属于同一个球队。这意味着两次往返数据库。如果你只使用一个查询,你可以减少一半,例如给定以下查询:
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 手册中找到的内容相对应。
没有(干净的)方法可以在 table 中定义约束来执行此操作。约束只能访问定义它们的 table 。某些约束只能从它们定义的内容访问列,仅此而已 (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: If you're curious, the very dirty way of doing this in a
CHECK
constraint is by defining a PostgreSQL function that basically bypasses the limitation. I wouldn't recommend it.
希望对您有所帮助:)