はじめに
実装したい機能の参考にectoのEcto.Schema
周りを読みました。その備忘録的に書いていきたいと思います。
ちなみにバージョンは2.2.11になります(v3.0.0-devまで出てますが、今回は2.2.11に)
読んだところ
lib/ecto/schema.ex
ここ読みました。
Schema
Schema macroの利用
まずは一番最初、schemaの宣言から
defmacro schema(source, [do: block]) do
schema(source, true, :id, block)
end
最初はマクロ宣言から。ここからすぐにschema関数に入ります。
そしてschema関数内をすぐにquote
で囲んでメタプロをしていきます。
defp schema(source, meta?, type, block) do
quote do
@after_compile Ecto.Schema
Module.register_attribute(__MODULE__, :changeset_fields, accumulate: true)
Module.register_attribute(__MODULE__, :struct_fields, accumulate: true)
まずは最初に@after_compile
で Ecto.Schema
がコンパイルされた後にコンパイルされるように宣言します。
この、「Ecto.Schema
がコンパイルされた後にコンパイルされる」っとあるが、どれがEcto.Schema
の後にコンパイルされるかというとdefmacro schema
を利用したモジュールがEcto.Schema
の後にコンパイルされます。
例としてサンプルのスキーマを記述しておきます。
defmodule SampleProject.User do
use Ecto.Schema
import Ecto.Changeset
schema "users" do
field(:name, :string)
field(:password, :string)
field(:email, :string)
field(:role, :integer)
timestamps()
end
end
このSampleProject.User
モジュールがEcto.Schema
モジュールの後にコンパイルされます。(違ったらごめんなさい)
次に、モジュールにアトリビュートを追加します。
changeset_fields
はfield
マクロで宣言したフィールドをいろいろセットしていきます(後から詳しく記載します)
struct_fields
は全体的なスキーマ定義の値になります。ここで入れた値を構造体として宣言します。以下のはサンプルとして作ったものになります。このような構造体になります。
%SampleProject.User{
__meta__: #Ecto.Schema.Metadata<:built, "users">,
email: nil,
id: nil,
inserted_at: nil,
name: nil,
password: nil,
role: nil,
updated_at: nil
}
changeset_fields
とstruct_fields
のアトリビュート値になります。
IO.inspect @changeset_fields
[
updated_at: :naive_datetime,
inserted_at: :naive_datetime,
role: :integer,
email: :string,
password: :string,
name: :string,
id: :id
]
IO.inspect @struct_fields
[
updated_at: nil,
inserted_at: nil,
role: nil,
email: nil,
password: nil,
name: nil,
id: nil,
__meta__: #Ecto.Schema.Metadata<:built, "users">
]
引数の値をquote
内で使うためunquote
します。
meta? = unquote(meta?)
source = unquote(source)
prefix = @schema_prefix
meta?
には schema
のマクロで宣言するとtrue
が入ります。
source
にはスキーマ名が入ります。
ここで、スキーマがメタプロなのかどうかif meta? do
で判定し、スキーマ名がbinary
かどうかチェックします。(""で囲まれたのはElixirはバイナリになります)
if meta? do
unless is_binary(source) do
raise ArgumentError, "schema source must be a string, got: #{inspect source}"
end
Module.put_attribute(__MODULE__, :struct_fields,
{:__meta__, %Metadata{state: :built, source: {prefix, source}}})
end
ectoのprimary key
if @primary_key == nil do
@primary_key {:id, unquote(type), autogenerate: true}
end
primary_key_fields =
case @primary_key do
false ->
[]
{name, type, opts} ->
Ecto.Schema.__field__(__MODULE__, name, type, [primary_key: true] ++ opts)
[name]
other ->
raise ArgumentError, "@primary_key must be false or {name, type, opts}"
end
プライマリーキーの宣言です。schemaモジュール内で@primary_key
が宣言されてなかった場合は{:id, unquote(type), autogenerate: true}
で宣言します。
なので、@primary_key
を宣言すればid
以外をプライマリーキーとして利用することができます。
Ecto.Schema.__field__(__MODULE__, name, type, [primary_key: true] ++ opts)
でプライマリーキーとしてフィールドに追加します。
[primary_key: true]
っというのでこのフィールドがプライマリーキーっとして利用するようにします。ここの第4引数はフィールドのオプションになります。
例えば、上記のuserスキーマでname
をプライマリーキーとして利用したい場合は、以下のようにするとプライマリーキーとして利用できます。
field(:name, :string, primary_key: true)
プライマリーキーになったかどうかは@ecto_primary_keys
の値をみればプライマリーキーになったかどうかわかります。
IO.inspect @ecto_primary_keys
[:name, :id]
@ecto_primary_keys
アトリビュートにプライマリーキーとして利用されるフィールドが配列として入ります。後々ここの値を使ってプライマリーキーの宣言をしていきます。
コードブロック内の評価
次にschema
のコードブロック内の評価を行っていきます。
try do
import Ecto.Schema
unquote(block)
after
:ok
end
unquote(block)
でschema
のdo ・・・ end
内のブロックを評価します。コードブロック内はfield
のマクロを実行していきます。
defmacro field(name, type \\ :string, opts \\ []) do
quote do
Ecto.Schema.__field__(__MODULE__, unquote(name), unquote(type), unquote(opts))
end
end
@doc false
def __field__(mod, name, type, opts) do
virtual? = opts[:virtual] || false
check_type!(name, type, virtual?)
pk? = opts[:primary_key] || false
check_type!
でtype
が正しいタイプかチェックします。
pk?
にこのフィールドがプライマリーキーかどうかチェックするための値を入れます。上で記述したようにオプションにprimary_key: true
があった場合はプライマリーキーとして利用されます。
default = default_for_type(type, opts)
Module.put_attribute(mod, :changeset_fields, {name, type})
put_struct_field(mod, name, default)
default_for_type
でデフォルト値を設定します。
defp default_for_type(_, opts) do
Keyword.get(opts, :default)
end
ここでもオプションにdefault
の値を返します。ない場合はnil
になります。
Module.put_attribute(mod, :changeset_fields, {name, type})
でchangeset_fields
にフィールドの名前とタイプを入れます。
defp put_struct_field(mod, name, assoc) do
fields = Module.get_attribute(mod, :struct_fields)
if List.keyfind(fields, name, 0) do
raise ArgumentError, "field/association #{inspect name} is already set on schema"
end
Module.put_attribute(mod, :struct_fields, {name, assoc})
end
put_struct_field
でstruct_fields
アトリビュートに値を追加します。
これでstruct_fields
にフィールドの追加を行っていき、上記で記述したように最終的に構造体として宣言準備を行っていきます。
ここでまた__field__
関数に戻ります。ちょっと大きいですが、バーチャルフィールドがどうかチェックを行います。
unless virtual? do
source = opts[:source] || Module.get_attribute(mod, :field_source_mapper).(name)
if name != source do
Module.put_attribute(mod, :ecto_field_sources, {name, source})
end
if raw = opts[:read_after_writes] do
Module.put_attribute(mod, :ecto_raw, name)
end
case gen = opts[:autogenerate] do
{_, _, _} ->
store_mfa_autogenerate!(mod, name, type, gen)
true ->
store_type_autogenerate!(mod, name, source || name, type, pk?)
_ ->
:ok
end
if raw && gen do
raise ArgumentError, "cannot mark the same field as autogenerate and read_after_writes"
end
if pk? do
Module.put_attribute(mod, :ecto_primary_keys, name)
end
Module.put_attribute(mod, :ecto_fields, {name, type})
end
バーチャルフィールドではない場合、フィールド値として追加します。
ここではオートインクリメントの設定やプライマリーキーを設定するためecto_primary_keys
に値を追加したりします。
アトリビュートの値を設定
primary_key_fields = @ecto_primary_keys |> Enum.reverse
autogenerate = @ecto_autogenerate |> Enum.reverse
autoupdate = @ecto_autoupdate |> Enum.reverse
fields = @ecto_fields |> Enum.reverse
field_sources = @ecto_field_sources |> Enum.reverse
assocs = @ecto_assocs |> Enum.reverse
embeds = @ecto_embeds |> Enum.reverse
アトリビュート値を反転させておきます。
Module.eval_quoted __ENV__, [
Ecto.Schema.__defstruct__(@struct_fields),
Ecto.Schema.__changeset__(@changeset_fields),
Ecto.Schema.__schema__(prefix, source, fields, primary_key_fields),
Ecto.Schema.__types__(fields, field_sources),
Ecto.Schema.__dumper__(fields, field_sources),
Ecto.Schema.__loader__(fields, field_sources),
Ecto.Schema.__field_sources__(fields, field_sources),
Ecto.Schema.__assocs__(assocs),
Ecto.Schema.__embeds__(embeds),
Ecto.Schema.__read_after_writes__(@ecto_raw),
Ecto.Schema.__autogenerate__(@ecto_autogenerate_id, autogenerate, autoupdate)]
最終的にアトリビュート値をもとにスキーマの最終宣言やもろもろを宣言していきます。
一つ一つの関数内はまた別の機会に記述していきたいと思います。
まとめ
スキーマでのマクロの使い方とか、シンプルで読みやすかったです。
もうちょっと深く読んでみたいのがbelongs_to
とかhas_many
とかも今後読んでみたいと思います。
めちゃくちゃアトリビュート値を使っているのと、PhoenixのルーティングやAbsintheのobject
などもアトリビュート値を駆使して宣言していました。(ココらへんもどこかの機会にまとめようかと思います)
以上です、