primary key用のEcto.Changeset.unique_constraintラッパーを書いた

まとめ

  • 複合primaryキーでも単数でもindex名としては:PRIMARYなので書き方は同じ
  • primaryキーにunique_constraintを使ったときのエラー表示が分かりにくいので、ラッパーを書いた

調査

調査用スキーマ

  @primary_key false
  schema "profiles" do
    field :user_id, :integer, primary_key: true
    field :email,   :string, primary_key: true
    field :token,   :string, primary_key: true
  end

Primaryキーのconstraintの書き方

単数でも複合でもindex名は:PRIMAYなので書き方は同じです。
changeset |> Changeset.unique_constraint(:user_id, [name: :PRIMARY])

調査結果

エラーメッセージがわかりにくい

changeset.errors #=> [user_id: {"has already been taken", []}]

primaryキーはuser_id, email, tokenなのにuser_idについてしか言及されていない。

エラーメッセージを変更する

Changeset.unique_constraintの第2引数に渡すfield名は、デフォルトindex名の生成と、errorsのキー名にしか使われていないように見える。
このため、nameオプションでindex名を指定している場合は、好きな名前を指定できそうです。

changeset |> Changeset.unique_constraint(:primary_keys, [name: :PRIMARY])
changeset.errors #=> [primary_keys: {"has already been taken", []}]

しかし慣例として、field名を入れたほうが良さそうに見えます。
documentには以下のように記載されており、実装も変数名はfieldとなっています。

Notice that the first param is just one of the unique index fields, this will be used as the error key to the changeset errors keyword list.

このため、ライブラリ側が想定していない使い方は控え、messageオプションに全ての対象field名を記載することにしました。

primary_constraintの実装

目標

  • 調査用スキーマを使って、以下のエラーメッセージを得る
    • errorsのキー名は複合キーの先頭field

# changeset.errors => [user_id: {"Duplicate primary key [:user_id, :email, :token]", []}]

# 使い方
def changeset(struct, params \\ %{}) do
  struct
  |> Changeset.cast(params, @required_fields)
  |> primary_constraint() 
end

実装

  defmacro primary_constraint(changeset) do
    quote do
      alias Ecto.Changeset
      pkeys = __MODULE__.__schema__(:primary_key)
      first_field = hd pkeys
      Changeset.unique_constraint(unquote(changeset), first_field, [name: :PRIMARY, message: "Duplicate primary key #{inspect(pkeys)}"])
    end
  end

以上です。プロダクトに投入して使っています。

Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account log in.