mmag

ハマったことメモなど

Ecto 2.0.0-betaを試す (1)

はい。

Ectoはしばらく触っていなかったのですが、v2.0がもうすぐということで触ってみました第1弾。 まずはプロジェクトをセットアップして単純なinsertのテストを通すところまで。

TL;DR

こちらに今日時点のコードがあるので、細かいとこは飛ばしていきます。

github.com

プロジェクト作成

mix new play_ecto --sup

依存性はこんな。2.0.0-betaを試すと書きましたが、mix ecto.createやらmix ecto.dropが動きませんでした。すると最近のコミットで直っていそうだったので、じゃあHEAD使おうということにしました。

defp deps do
  [
    {:ecto, github: "elixir-lang/ecto", ref: "dc2d2455ded89683e383736358727b0d89e2c9e6"},
    {:postgrex, ">= 0.0.0"}
  ]
end

migration作成

mix ecto.gen.migration add_users_table
defmodule PlayEcto.Repo.Migrations.AddUsersTable do
  use Ecto.Migration

  def change do
    create table(:users) do
      add :name,          :string
      add :password_hash, :string

      timestamps
    end

    create index(:users, [:name])
  end
end
mix ecto.migrate

この辺は以前と変わってないかな。

Userモジュール

defmodule PlayEcto.User do
  use Ecto.Schema
  import Ecto.Changeset

  schema("users") do
    field :name,          :string
    field :password,      :string, virtual: true
    field :password_hash, :string

    timestamps
  end

  @allowed ~w[name password]

  def changeset(model, params) do
    model
    |> cast(params, @allowed)
    |> hash_password
    |> validate_required(:name)
    |> validate_required(:password_hash)
  end

  defp hash_password(changeset) do
    case get_change(changeset, :password) do
      nil      -> changeset
      password -> put_change(changeset, :password_hash, hashing(password))
    end
  end

  defp hashing(_password), do: "hogehogefugafuga"
end

私がちょっと触ってたときとは結構変わっているようです。 以前はcast/4で、第3、第4引数にそれぞれ必須フィールドと任意フィールドの名前をリストで与えていましたが、deprecatedになったようです。代わりにcast/3を使い、第3引数には許可するパラメータ名をリストで与えます。

テスト

以下のテストをGreenにしましょう。

defmodule PlayEctoTest do
  use ExUnit.Case

  alias PlayEcto.{User, Repo}

  test "User operation" do
    changeset = User.changeset(%User{}, %{name: "タナカ", password: "tanaka1234"})

    Repo.insert!(changeset)
    user = Repo.get_by(User, name: "タナカ")

    assert user.name == "タナカ"
    assert user.password      |> is_nil
    assert user.password_hash |> is_binary
  end
end

まずDBが無いと怒られるので、test_helper.exsにこんな2行を追加します。これはphoenixのプロジェクトから持ってきました。初回のmix testはコケて、2回目からはテストが走るようになりました。なんででしょ。

Mix.Task.run "ecto.create", ~w(-r PlayEcto.Repo --quiet)
Mix.Task.run "ecto.migrate", ~w(-r PlayEcto.Repo --quiet)

Ecto.Adapters.SQL.begin_test_transaction(PlayEcto.Repo) という1行も入れてみたのですが、そんな関数は無いと言われたので消しました。 このままでは、テスト中にinsertした行がテスト後も残ってしまうのでは、と思ったらやっぱり残りました。

さてどうしたものかと思い、本家はどうやってテストしてるのかな〜と見てみると怪しいコードが。

ecto/test_helper.exs at dc2d2455ded89683e383736358727b0d89e2c9e6 · elixir-lang/ecto · GitHub

最初から読めという感じですが、Sandboxのドキュメントに色々と書いてありました。test_helper.exsの最後には以下の行を書くべきだそうです。

Ecto.Adapters.SQL.Sandbox.mode(PlayEcto.Repo, :manual)

ドキュメントによると、オーナーシップの考え方が導入されたそうです。checkoutするとプールされたコネクションが貰えてトランザクションが走ります。このコネクションはcheckoutしたプロセスしか明示的に許可しない限り使えません。で、なにが変わったのかというと、これまではDBの読み書きを含むようなテストをするときはasync: falseを付けてシーケンシャルに行わなければならなかったのですが、それらが並行にテストできるようになったのでした。すごい。

次はリレーション辺りを攻めようかな。

テーマはFB matteをベースにしてます。作者さんに感謝を込めて。