Elixir/Phoenix/Ecto中如何添加关联对象并验证子对象及关联长度?
如何用Ecto实现带关联验证的Player插入
1. 完善Schema的基础验证
先给Game和Player的Schema添加必填字段的基础验证:
Game Schema
defmodule Game do use Ecto.Schema import Ecto.Changeset alias __MODULE__ schema "games" do field :game_name, :string has_many :players, Player timestamps(type: :utc_datetime) end def changeset(%Game{} = game, attrs) do game |> cast(attrs, [:game_name]) |> validate_required([:game_name], message: "游戏名称不能为空") end end
Player Schema
defmodule Player do use Ecto.Schema import Ecto.Changeset alias __MODULE__ schema "players" do field :player_name, :string belongs_to :game, Game timestamps(type: :utc_datetime) end def changeset(%Player{} = player, attrs) do player |> cast(attrs, [:player_name, :game_id]) |> validate_required([:player_name, :game_id], message: "玩家名称不能为空") |> assoc_constraint(:game, message: "关联的游戏不存在") end end
这里的assoc_constraint(:game)会自动验证game_id对应的Game是否存在,同时会触发数据库外键约束(需确保数据库表已配置外键)。
2. 实现游戏玩家数量上限验证
推荐把业务逻辑放在Phoenix的Context模块中(类似Rails的Service层),让职责更清晰:
创建PlayerContext模块
defmodule MyApp.PlayerContext do alias MyApp.Player alias MyApp.Game alias MyApp.Repo def create_player_for_game(game_id, player_attrs) do # 查询目标游戏并预加载已有玩家 case Repo.get(Game, game_id) |> Repo.preload(:players) do nil -> {:error, :game_not_found} game -> if length(game.players) >= 4 do {:error, :max_players_reached} else # 合并game_id到玩家参数,构建changeset并插入 player_params = Map.put(player_attrs, "game_id", game_id) %Player{} |> Player.changeset(player_params) |> Repo.insert() end end end end
3. 在Controller中处理请求
在Phoenix Controller里调用Context函数,处理POST请求并返回响应:
defmodule MyAppWeb.PlayerController do use MyAppWeb, :controller alias MyApp.PlayerContext def create(conn, %{"game_id" => game_id, "player" => player_attrs}) do case PlayerContext.create_player_for_game(game_id, player_attrs) do {:ok, player} -> conn |> put_status(:created) |> json(%{data: %{id: player.id, name: player.player_name, game_id: player.game_id}}) {:error, :game_not_found} -> conn |> put_status(:not_found) |> json(%{error: "指定的游戏不存在"}) {:error, :max_players_reached} -> conn |> put_status(:unprocessable_entity) |> json(%{error: "该游戏玩家数量已达上限(最多4人)"}) {:error, changeset} -> conn |> put_status(:unprocessable_entity) |> json(%{errors: format_changeset_errors(changeset)}) end end # 辅助函数:格式化Changeset错误信息 defp format_changeset_errors(changeset) do Ecto.Changeset.traverse_errors(changeset, fn {msg, _opts} -> msg end) end end
关键说明
- Ecto Changeset核心差异:和Rails的ActiveRecord不同,Ecto Changeset是纯函数式设计,所有验证操作返回新的Changeset对象,不修改原数据,逻辑更可控、易测试。
- 关联验证方式:
assoc_constraint:用于验证关联记录存在性,底层依赖数据库外键约束,避免脏数据。- 自定义业务验证:玩家数量上限通过查询关联Game并统计玩家数实现,放在Context中让职责分离更清晰。
- 参数处理:将POST请求中的
game_id合并到玩家参数中,确保Changeset能获取关联ID完成验证。
内容的提问来源于stack exchange,提问作者Chowlett
相关产品推荐
相关产品推荐

