エンジニアのソフトウェア的愛情

または私は如何にして心配するのを止めてプログラムを・愛する・ようになったか

Ectoを使ってデータベースを操作する (v2.2)

最初に。

昨年 2018 年 6 月に Ecto の使い方について書いていたのですが、書きかけのまま放置しているうちにメジャーバージョンが上がってしまい、最新の 3.0 ではここに書いてある内容が当てはまらなくなるという事態になりました。なにより sqlite_ecto2 アダプタが(その名の通り)Ecto のバージョン 2 向けのため、Ecto バージョン 3 では利用できません。

とはいえ、まだバージョン 2 が利用できなくなったわけでないですし、アダプタの対応などを除けば使い方はあまり変わらないので、覚書として公開しておきます。リンクのみバージョン 2 の最新 2.2.11 のドキュメントを指すように変更しました。

2 と 3 の違いについては 最新のドキュメントGitHubの CHANGELOG.md などを確認してください。

バージョン 3 についてもそのうち書くと思います。たぶん。

Ecto 2.2

Phoenix ではデータベースの操作のために標準で Ecto を利用するので、作成したプロジェクトは Ecto を利用するための設定が済んでいます。これを手作業で設定してみる試みです。

設定までの手順の話になりますのですので、Ecto の利用方法自体はドキュメントを読んでみてください。

印象としては、意外に簡単に利用することができました。が、意外にコードを書いた気分。

ドキュメント

公式ドキュメントです。ここを読めばだいたい使えるようになります。

手順

プロジェクトを作る

mix new で新しいプロジェクトを用意します。Ecto ではデータベースの操作のためのプロセスを起動することになるので、--sup オプションを指定しておきます。

$ mix new my_db --sup
$ cd my_db

依存パッケージに Ecto を追加する

mix.exs に Ecto を追加します。

  defp deps do
    [
      {:ecto, "~> 2.2"}
    ]
  end

パッケージを取得してコンパイルします。

$ mix do deps.get, deps.compile

Ecto を利用するための各種コマンドが追加されます。 mix ecto で利用できるコマンドの一覧が表示されます。

$ mix ecto
Ecto v2.2.10
A database wrapper and language integrated query for Elixir.

Available tasks:

mix ecto.create        # Creates the repository storage
mix ecto.drop          # Drops the repository storage
mix ecto.dump          # Dumps the repository database structure
mix ecto.gen.migration # Generates a new migration for the repo
mix ecto.gen.repo      # Generates a new repository
mix ecto.load          # Loads previously dumped database structure
mix ecto.migrate       # Runs the repository migrations
mix ecto.migrations    # Displays the repository migration status
mix ecto.rollback      # Rolls back the repository migrations

リポジトリを作成する

mix ecto.gen.repo コマンドでリポジトリを作成します。

$ mix ecto.gen.repo -r MyDb.Repo
* creating lib/my_db
* creating lib/my_db/repo.ex
* updating config/config.exs
Don't forget to add your new repo to your supervision tree
(typically in lib/my_db/application.ex):

    supervisor(MyDb.Repo, [])

And to add it to the list of ecto repositories in your
configuration files (so Ecto tasks work as expected):

    config :my_db,
      ecto_repos: [MyDb.Repo]

lib/my_db/repo.ex が作成され、config/config.exs にデータベースにアクセスするための設定が追加されます。 デフォルトでは PostgreSQL を利用する設定になっています。

config :my_db, MyDb.Repo,
  adapter: Ecto.Adapters.Postgres,
  database: "my_db_repo",
  username: "user",
  password: "pass",
  hostname: "localhost"

作成時のメッセージにあるように、config/config.exs に設定を追加します。

config :my_db,
  ecto_repos: [MyDb.Repo]

アプリケーションが起動した時に Ecto のプロセスも起動するように、 lib/my_db/application.ex を編集して、監視するプロセスに MyDb.Repo を追加します。

  def start(_type, _args) do
    children = [
      MyDb.Repo
    ]

    opts = [strategy: :one_for_one, name: MyDb.Supervisor]
    Supervisor.start_link(children, opts)
  end

アダプタを追加する

データベースを操作するために Ecto 以外にアダプタのパッケージが必要になります。 自動的に追加された設定では PostgreSQL を利用するようになっていますが、ここでは SQLite3 を使ってみます。データベースがファイルに保存されて、プロジェクトのディレクトリで完結するので後始末が楽なので。

Hexsqlite で検索すると、Ecto 用のアダプタとして sqlite_ecto2 がヒットします。これを利用します。

mix.exs を編集します。

  defp deps do
    [
      {:ecto, "~> 2.2"},
      {:sqlite_ecto2, "~> 2.2"}
    ]
  end

パッケージを取得します。

$ mix deps.get

config/config.exs を編集します。

config :my_db, MyDb.Repo,
  adapter: Sqlite.Ecto2,
  database: "my_db.sqlite3"

テーブルを追加する

mix ecto.gen.migration コマンドを利用して、テーブルを追加するマイグレーションファイルを作成します。

$ mix ecto.gen.migration books
Generated my_db app
* creating priv/repo/migrations
* creating priv/repo/migrations/20180619123954_books.exs

先頭に日付と時刻の数字の並びを持つマイグレーションファイルが作成されますので、ここにマイグレーションを記述します。 マイグレーションの記述方法は Ecto.Migration のドキュメントに記載されています。

ここではテーブル books の作成を記述しました。

defmodule MyDb.Repo.Migrations.Books do
  use Ecto.Migration

  def change do
    create table("books") do
      add :title, :string, null: false
      add :bought_at, :naive_datetime
      add :price, :integer
he
      timestamps()
    end
  end
end

マイグレーションを実行します。

$ mix ecto.migrate

スキーマファイルを作成する

テーブル books を Ecto で操作するためのスキーマファイル lib/my_db/book.ex を作成します。

スキーマの記述方法は Ecto.Schema のドキュメントに記載されています。

defmodule MyDb.Book do
  use Ecto.Schema

  schema "books" do
    field :title, :string
    field :bought_at, :naive_datetime
    field :price, :integer

    timestamps
  end
end

データベースを操作する

iex で操作してみます。

$ iex -S mix

books の全レコードを取得します。

iex(1)> MyDb.Repo.all(MyDb.Book)

21:54:48.558 [debug] QUERY OK source="books" db=41.6ms
SELECT b0."id", b0."title", b0."bought_at", b0."price", b0."inserted_at", b0."updated_at" FROM "books" AS b0 []
[]

まだレコードを作成していないので空のリストが返ります。

レコードを追加してみます。

iex(2)> MyDb.Repo.insert(%MyDb.Book{title: "ネコと鴎の王冠", price: 734, bought_at: ~N[2017-11-22 00:00:00]})

22:03:22.132 [debug] QUERY OK db=99.0ms queue=0.1ms
INSERT INTO "books" ("bought_at","price","title","inserted_at","updated_at") VALUES (?1,?2,?3,?4,?5) ;--RETURNING ON INSERT "books","id" [{{2017, 11, 22}, {0, 0, 0, 0}}, 734, "ネコと鴎の王冠", {{2018, 6, 19}, {13, 3, 22, 32306}}, {{2018, 6, 19}, {13, 3, 22, 32319}}]
{:ok,
 %MyDb.Book{
   __meta__: #Ecto.Schema.Metadata<:loaded, "books">,
   bought_at: ~N[2017-11-22 00:00:00],
   id: 1,
   inserted_at: ~N[2018-06-19 13:03:22.032306],
   price: 734,
   title: "ネコと鴎の王冠",
   updated_at: ~N[2018-06-19 13:03:22.032319]
 }}

ここで日時の指定には ~N sigil を利用してます。

もう一度全レコードを取得してみます。

iex(3)> MyDb.Repo.all(MyDb.Book)

22:05:54.874 [debug] QUERY OK source="books" db=1.1ms decode=0.1ms
SELECT b0."id", b0."title", b0."bought_at", b0."price", b0."inserted_at", b0."updated_at" FROM "books" AS b0 []
[
  %MyDb.Book{
    __meta__: #Ecto.Schema.Metadata<:loaded, "books">,
    bought_at: ~N[2017-11-22 00:00:00.000000],
    id: 1,
    inserted_at: ~N[2018-06-19 13:03:22.032306],
    price: 734,
    title: "ネコと鴎の王冠",
    updated_at: ~N[2018-06-19 13:03:22.032319]
  }
]

レコードをもう一つ登録。

iex(4)> MyDb.Repo.insert(%MyDb.Book{title: "時空のゆりかご", price: 1188, bought_at: ~N[2018-02-28 00:00:00]})

22:19:13.797 [debug] QUERY OK db=35.1ms
INSERT INTO "books" ("bought_at","price","title","inserted_at","updated_at") VALUES (?1,?2,?3,?4,?5) ;--RETURNING ON INSERT "books","id" [{{2018, 2, 28}, {0, 0, 0, 0}}, 1188, "時空のゆりかご", {{2018, 6, 19}, {13, 19, 13, 759710}}, {{2018, 6, 19}, {13, 19, 13, 759722}}]
{:ok,
 %MyDb.Book{
   __meta__: #Ecto.Schema.Metadata<:loaded, "books">,
   bought_at: ~N[2018-02-28 00:00:00],
   id: 2,
   inserted_at: ~N[2018-06-19 13:19:13.759710],
   price: 1188,
   title: "時空のゆりかご",
   updated_at: ~N[2018-06-19 13:19:13.759722]
 }}

全レコードを取得。

iex(5)> MyDb.Repo.all(MyDb.Book)

22:19:43.295 [debug] QUERY OK source="books" db=1.2ms decode=0.1ms
SELECT b0."id", b0."title", b0."bought_at", b0."price", b0."inserted_at", b0."updated_at" FROM "books" AS b0 []
[
  %MyDb.Book{
    __meta__: #Ecto.Schema.Metadata<:loaded, "books">,
    bought_at: ~N[2017-11-22 00:00:00.000000],
    id: 1,
    inserted_at: ~N[2018-06-19 13:03:22.032306],
    price: 734,
    title: "ネコと鴎の王冠",
    updated_at: ~N[2018-06-19 13:03:22.032319]
  },
  %MyDb.Book{
    __meta__: #Ecto.Schema.Metadata<:loaded, "books">,
    bought_at: ~N[2018-02-28 00:00:00.000000],
    id: 2,
    inserted_at: ~N[2018-06-19 13:19:13.759710],
    price: 1188,
    title: "時空のゆりかご",
    updated_at: ~N[2018-06-19 13:19:13.759722]
  }
]

レコード数を確認します。

iex(6)> MyDb.Book |> select([b], count(b.id)) |> MyDb.Repo.one()

22:20:12.655 [debug] QUERY OK source="books" db=1.0ms
SELECT count(b0."id") FROM "books" AS b0 []
2

だいたいこんな感じで。

いつか読むはずっと読まない:キツネと熊は何処