ectoのSchema周りを読んでみた

## はじめに

実装したい機能の参考にectoの`Ecto.Schema`周りを読みました。その備忘録的に書いていきたいと思います。
ちなみにバージョンは2.2.11になります（v3.0.0-devまで出てますが、今回は2.2.11に）

## 読んだところ

```
lib/ecto/schema.ex
```
ここ読みました。

https://github.com/elixir-ecto/ecto/blob/v2.2.11/lib/ecto/schema.ex

## Schema
### Schema macroの利用

まずは一番最初、schemaの宣言から

```elixir:schema.ex
defmacro schema(source, [do: block]) do
  schema(source, true, :id, block)
end
```
最初はマクロ宣言から。ここからすぐにschema関数に入ります。
そしてschema関数内をすぐに`quote`で囲んでメタプロをしていきます。

```elixir:schame.ex
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`の後にコンパイルされます。
例としてサンプルのスキーマを記述しておきます。

```elixir:user.ex
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` は全体的なスキーマ定義の値になります。ここで入れた値を構造体として宣言します。以下のはサンプルとして作ったものになります。このような構造体になります。

```elixir:user.ex
%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`のアトリビュート値になります。

```elixir
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`します。

```elixir:schema.ex
meta?  = unquote(meta?)
source = unquote(source)
prefix = @schema_prefix
```
`meta?`には `schema` のマクロで宣言すると`true`が入ります。
`source`にはスキーマ名が入ります。

ここで、スキーマがメタプロなのかどうか`if meta? do`で判定し、スキーマ名が`binary`かどうかチェックします。(""で囲まれたのはElixirはバイナリになります）

```elixir:schema.ex
  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

```elixir:schema.ex
  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]`っというのでこのフィールドがプライマリーキーっとして利用するようにします。ここの第４引数はフィールドのオプションになります。
例えば、上記のuserスキーマで`name`をプライマリーキーとして利用したい場合は、以下のようにするとプライマリーキーとして利用できます。

```elixir
field(:name, :string, primary_key: true)
```
プライマリーキーになったかどうかは`@ecto_primary_keys`の値をみればプライマリーキーになったかどうかわかります。

```elixir
IO.inspect @ecto_primary_keys
[:name, :id]
```
`@ecto_primary_keys` アトリビュートにプライマリーキーとして利用されるフィールドが配列として入ります。後々ここの値を使ってプライマリーキーの宣言をしていきます。

### コードブロック内の評価

次に`schema`のコードブロック内の評価を行っていきます。

```elixir:schema.ex
  try do
    import Ecto.Schema
    unquote(block)
  after
    :ok
  end
```

`unquote(block)`で`schema`の`do ・・・ end`内のブロックを評価します。コードブロック内は`field`のマクロを実行していきます。

```elixir:schema.ex
  defmacro field(name, type \\ :string, opts \\ []) do
    quote do
      Ecto.Schema.__field__(__MODULE__, unquote(name), unquote(type), unquote(opts))
    end
  end
```

```elixir:schema.ex
  @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`があった場合はプライマリーキーとして利用されます。

```elixir:schema.ex
  default = default_for_type(type, opts)
  Module.put_attribute(mod, :changeset_fields, {name, type})
  put_struct_field(mod, name, default)
```
`default_for_type`でデフォルト値を設定します。

```elixir:schema.ex
  defp default_for_type(_, opts) do
    Keyword.get(opts, :default)
  end
```

ここでもオプションに`default`の値を返します。ない場合は`nil`になります。
`Module.put_attribute(mod, :changeset_fields, {name, type})`で`changeset_fields`にフィールドの名前とタイプを入れます。

```elixir:schema.ex
  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__`関数に戻ります。ちょっと大きいですが、バーチャルフィールドがどうかチェックを行います。

```elixir
  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`に値を追加したりします。

### アトリビュートの値を設定

```elixir
  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
```
アトリビュート値を反転させておきます。

```elixir
  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`などもアトリビュート値を駆使して宣言していました。（ココらへんもどこかの機会にまとめようかと思います）

以上です、