Posted at

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

More than 1 year has passed since last update.


まとめ


  • 複合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

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