最初に。
昨年 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(, ) do
children = [
MyDb.Repo
]
opts = [strategy: :one_for_one, name: MyDb.Supervisor]
Supervisor.start_link(children, opts)
end
アダプタを追加する
データベースを操作するために Ecto 以外にアダプタのパッケージが必要になります。
自動的に追加された設定では PostgreSQL を利用するようになっていますが、ここでは SQLite3 を使ってみます。データベースがファイルに保存されて、プロジェクトのディレクトリで完結するので後始末が楽なので。
Hex で sqlite
で検索すると、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
だいたいこんな感じで。
いつか読むはずっと読まない:キツネと熊は何処