You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.14 04:18:24