5
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Elixir Ecto.Typeでcuctom typeの作成 atomのTupleを保存してみる

Posted at

なぜTupleを保存?

Exlixir勉強中です。

勉強用にオセロゲームを作ってみました。盤面はTupleで保存しています。
タプルの中身は、:none, :black , :whiteのいずれか。これで盤面を表しています。
Ectoで盤面を保存して、棋譜の履歴を保存を実現したい。

Tupleを保存したい。

Tupleを使わず、ListやMapで盤面を保存していれば、型を作らなくても問題ないと思いますが、勉強も兼ねて、Tupleに挑戦してみます。

実はこんな方法もあるよ。などコメントいただけると嬉しいです。

修正前

かなり、ゴチャゴチャです。
盤面はBoard.cellsにTuple型で保存されています。
Tupleを含んでいるBoardは、changesetに受け付けられないので、Tuple.to_list でリストに変換。
Atomを数値に変換するconvert_cell_to_integer()かませてます

  def push(board = %Board{}) do
    id = board.id
    cells = Tuple.to_list(board.cells)
    |> Enum.map(&convert_cell_to_integer(&1))
    board_map = %{board | cells: cells, id: id, turn: convert_cell_to_integer(board.turn)}|> Map.from_struct()
    new_data = changeset(%__MODULE__{},board_map)
    from(log in __MODULE__, where: log.id >= ^id) |> Repo.delete_all
    if id > 0, do: Repo.insert(new_data)
    board
  end

Ecto.Typeを使ってEctoTupleAtom型を作り、変換処理をEcto側に持っていきます。

Ecto.Type/ParameterizedTypeの試行錯誤

本家のドキュメントを見て簡単にできるかと思ったんですが、castってなに?dumpとどんな関係なの?という疑問で、ハマりました。この点をまとめておきます。

callback 入力→出力 コメント
cast() 任意の型→dumpが受け付ける型 Tuple→Tuple dumpが受け付ける型に変換(前処理)。受け付けない型は拒否
dump() castが出力する型→Ecto native Tuple→:string DBに書き込むデータに変換する
load() Ecto native→任意の型 :string→Tuple DBから読んだ生の値を、要望の型に変換する

Ecto nativeの型は、フィールドに設定されている型(:integerや、:string等)と考えてよいかと思います。

今回は、castでは、何も処理しませんでした(tupleであるかの確認だけ)が、listでもtupleでも受け付けるようにする場合、listを受け取り、tupleで出力するcastを追加すれば両方に対応できます。

基本、cast dumb loadのcallbackを書くだけなので、この役割分担がわかれば、あとは一直線でした。

Ecto.ParameterizedTypeの実装

Ecto.TypeとEcto.ParameterizedTypeの違いは、パラメータがあるか無いかの違いです。
atomの変換をパラメータとし持たせたかったので、Ecto.ParameterizedTypeを使います。
パラメータを受け付ける、init()追加で必要になります。

EctoTupleAtomの実装は次の通りです。

ecto_tuble_atom.ex
defmodule EctoTupleAtom do
  use Ecto.ParameterizedType

  def type(_params), do: :string

  def init(opts) do
    forward_map = Enum.into(opts[:values], %{})
    reverce_map = Map.new(forward_map, fn {key, val} -> {val, key} end)
    %{f_map: forward_map, r_map: reverce_map}
  end

  def cast(cells, _params) when is_tuple(cells) do
    {:ok, cells}
  end

  def load(data, _loader, params) when is_binary(data) do
    {:ok,
     data
     |> String.split("", trim: true)
     |> Enum.map(fn x -> params.r_map[x] end)
     |> List.to_tuple()}
  end

  def dump(cells, _dumper, params) when is_tuple(cells) do
    {:ok,
     cells
     |> Tuple.to_list()
     |> Enum.map(fn x -> params.f_map[x] end)
     |> Enum.join("")}
  end

  def dump(nil, _, _), do: {:ok, ""}

  def dump(_, _, _), do: :error
end

Schema

EctoTupleAtomをschemaに組み込んで使ってみます。valuesにAtomと対応する文字のリストを指定します。

log.ex
defmodule HexReversi.BoardHistory.Log do
  use Ecto.Schema
  import Ecto.Changeset

  schema "logs" do
    field(:xs, :integer)
    field(:ys, :integer)
    field(:cells, EctoTupleAtom, values: [none: "0", black: "1", white: "2"])
    field(:gameover, :boolean)
    field(:turn, Ecto.Enum, values: [none: "0", black: "1", white: "2"])
    field(:white, :integer)
    field(:black, :integer)

    timestamps()
  end

  @doc false
  def changeset(log, attrs) do
    log
    |> cast(attrs, [:xs, :ys, :cells, :gameover, :turn, :white, :black, :id])
    |> validate_required([:cells])
  end
end

変換の様子

dumpとloadにIO.Inspectでデータを入れて、データの変換の様子をみてみました。
実行した結果は以下の通りです。
atomを文字に変換してつなげたものになっています。

dump_in: {:none, :none, :black, :white}
dump_out: "0012"
load_in: "0012"
load_out: {:none, :none, :black, :white}

EctoTupleAtom適用後のプログラム

データの変換処理が不要になり、すっきりできました。
pushでは、delete()とinsert()を行てたので、分けてみました。
修正前に比べて、かなり見通しが良くなりました。

  def push(board = %Board{}) do
    %{board | id: board.id + 1}
    |> delete()
    |> insert()
  end

  def delete(board = %Board{}) do
    id = board.id
    from(log in Log, where: log.id >= ^id) |> Repo.delete_all()
    board
  end

  def insert(board = %Board{}) do
    new_data = Log.changeset(%Log{}, Map.from_struct(board))
    if board.id > 0, do: Repo.insert(new_data)
    board
  end

Ecto.Enumについて

当初、atomをDBで保存処理を、Ecto.Enumを参考にしてできないか検討しました。
Ecto.Enumのソースを調べました。
残念ながら、うまくこの機能を持ってくることはできませんでしたが、cast/dumpがどのように使うか参考になりました。

cast

基本はAtomが入力になる
Atom以外にvalue(変換後の値)や Atom.to_string(key)も受け付ける。
想定外の入力があった場合には、これをAtomに変換する処理するcastを作成しています。

/lib/ecto_enum/use.ex:29
      for {key, value} <- opts, k <- Enum.uniq([key, value, Atom.to_string(key)]) do
        def cast(unquote(k)), do: {:ok, unquote(key)}
      end

dump

atomから目的の値に変換するdumpを作成しています。

/lib/ecto_enum/use.ex:35
      for {key, value} <- opts, k <- Enum.uniq([key, value, Atom.to_string(key)]) do
        def dump(unquote(k)), do: {:ok, unquote(value)}
      end

値の変換のメインはdump。castは前処理という位置づけ

まとめ

Tupleを保存する型を作ることで、データの保存処理が簡素に書けるようになった。

参考
https://hexdocs.pm/ecto/Ecto.Type.html
https://hexdocs.pm/ecto/Ecto.ParameterizedType.html
https://hexdocs.pm/ecto/Ecto.Enum.html
https://github.com/gjaldon/ecto_enum

5
1
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?