Rubyに影響を受けた(というより弟分)言語であるElixirが熱いです。
RailsインスパイアのElixirのWAFであるPhoenixはrailsの10倍早いらしいです。
何かと話題の関数型言語elixir + phoenixを勉強中にデータベースラッパー&クエリジェネレータであるEctoの使い方がrailsのActiveRecordとは違う雰囲気で色々と悩んだのでメモ。Ecto単体で使った場合なのでphoenixと一緒に使った場合は違う部分があるかもしれません。
全体的にパイプライン演算子(|>
)を存分に使ったコードで楽しいです。キーワードクエリはSQLっぽくてどうにも好きになれないので基本的にパイプラインクエリを使っています。
「こっちの方が適切」「もっとイケてる書き方がある」「phoenixではこう書ける」「phoenixでは使えない」等ありましたらコメントお願いします。
準備
phoenixであればmix phoenix.new
でこの辺りはだいたい勝手に生成されます。以下はEcto単体での話です。
mix new blog --sup
でプロジェクトを作成。
依存にecto
とpostgrex
を追加
defp deps do
[
{:ecto, "~> 2.0"},
{:postgrex, "0.11.2"}
]
end
依存解決
$ mix deps.get
mix.exs
のapplicationにpostgrex
追加
def application do
[applications: [:logger, :postgrex],
mod: {Blog, []}]
end
設定ファイルの追記とlib/blog/repo.ex
の生成。
$ mix ecto.gen.repo -r Blog.Repo
config/config.exs
に設定を追加
config :blog, Blog.Repo,
adapter: Ecto.Adapters.Postgres,
database: "blog_repo",
username: "postgres",
password: "postgres",
hostname: "localhost"
config :blog, ecto_repos: [Blog.Repo]
lib/blogs.ex
にスーパーバイザの設定
def start(_type, _args) do
import Supervisor.Spec, warn: false
children = [
worker(Blog.Repo, []),
]
...
データベースの作成
$ mix ecto.create
マイグレーション
$ mix ecto.gen.migration create_users
defmodule Blog.Repo.Migrations.CreateUsers do
use Ecto.Migration
def change do
create table(:users) do
add :email, :string
add :name, :string
timestamps
end
end
end
$ mix ecto.gen.migration create_articles
defmodule Blog.Repo.Migrations.CreateArticles do
use Ecto.Migration
def change do
create table(:articles) do
add :title, :string
add :body, :text
add :user_id, references(:users)
timestamps
end
end
end
$ mix ecto.migrate
スキーマ
defmodule Blog.User do
use Ecto.Schema
schema "users" do
field :email, :string
field :name, :string
timestamps
has_many :articles, Blog.Article
end
end
defmodule Blog.Article do
use Ecto.Schema
schema "articles" do
field :title, :string
field :body, :string
timestamps
belongs_to :user, Blog.User
end
end
schema
の中でhas_many
, belongs_to
することでassociationを定義しています。
スキーマで使えるカラムのタイプは
:id
:binary_id
:integer
:float
:boolean
:string
:binary
{:array, inner_type}
:decimal
:map
{:map, inner_type}
これらに加えて、カスタムタイプを作ることもできるようです。
デフォルトでは
:datetime
:date
:time
:uuid
があるようです。
.iex.exs
iexで色々試す時に予めimport
, alias
等をしたい場合は.iex.exs
をカレントディレクトリかホームディレクトリに置くと読み込んでくれるのでiexでよく使うものはここに書いておきます。
import Ecto.Query, except: [preload: 2]
import Blog.Repo
alias Blog.User
alias Blog.Article
import
, require
, use
, alias
については以下を参考
alias, require, importの違いを理解してみる
Elixir の require, imoport, use
ActiveRecord vs Ecto
get, all, get_by
railsで言うUser.find(1)
User |> get(1)
railsで言うUser.all
User |> all
railsで言うUser.find_by(name: "国木田花丸")
User |> get_by(name: "国木田花丸")
get
, all
, get_by
を修飾名無し呼べるのは.iex.exs
でimport Blog.Repo
しているためです。iex以外で使う時はimport Blog.Repo
するか、修飾名で呼んで下さい。
クエリ構築 & 取得
railsで言うUser.where(name: "黒澤ルビィ")
User |> where(name: "黒澤ルビィ") |> all
where
はimport Ecto.Query
しているためです。
User |> where(name: "zura")
でクエリを構築して、all
でレコードを取得するイメージです。
クエリ構築で止めることで、合成可能なクエリを構築できます。
もちろんgroup_by
, having
, join
, limit
, offset
, order_by
などもあります。
ちなみに、取得件数が1件の時はall
の代わりにone
を使います。
Ecto2.0からのパイプラインによるクエリ構築
railsで言うArticle.where(user_id: 1).order(:created_at).limit(5)
Article |> where(user_id: 1) |> order_by(:inserted_at) |> limit(5)
# #Ecto.Query<from a in Blog.Article, where: a.user_id == 1,
# order_by: [asc: a.inserted_at], limit: 5>
where
, order_by
, limit
はimport Ecto.Query
(ry。
ただしselect
する時はselect([a], a.body)
のようなスタイルにする必要があるようです(select
自体あまり使うことはなさそうですが)
Ecto2.0以前のキーワードクエリやパイプラインクエリは妙に分かりづらいのでシンプルに表現できるこのスタイルは非常に嬉しいです。
ちなみにキーワードクエリと以前のパイプラインクエリによる同等のクエリ構築は以下のようです。
# キーワードクエリ
from a in Article, where: a.user_id == 1, order_by: [desc: a.inserted_at], limit: 5
# 以前のパイプラインクエリ
Article |> where([a], a.user_id == 1) |> order_by([a], desc: a.inserted_at) |> limit(5)
参考 Ecto v1.1 released and Ecto v2.0 plans « Plataformatec Blog
has_many
のassociationを取り出す
railsで言うuser.articles
user = User |> get(1)
articles = user |> Ecto.assoc(:articles) |> all
associationをロードする
user = User |> get(1) |> preload(:articles)
user.articles
# [%Blog.Article{..}, ..]
preload
はimport Blog.Repo
(ry
Ecto.Query
にもpreload
という関数があるので、.iex.exs
ではexceptしました。
belongs_to
のassociationを取り出す
railsで言うarticle.user
article = Article |> get(1) |> preload(:user)
user = article.user
# 一行で書ける
user = Article |> get(1) |> preload(:user) |> Map.get(:user)
なんというか惜しい。もうちょっと綺麗な書き方ないのですかね?
追記
Article |> get(1) |> Ecto.assoc(:user) |> one
でいけました。
association構築
railsで言うuser.articles.new
user = User |> get(1)
user |> Ecto.build_assoc(:articles)
# 同時にattributesも指定
user |> Ecto.build_assoc(:articles, title: "ruby", body: "がんばルビィ")
insert & update & delete
railsで言うUser.create
, User.update
, User.delete
# insert
%User{email: "zura@gmail.com", name: "国木田花丸"} |> insert
# update
%User{id: 1, email: "zura@gmail.com", name: "国木田花丸"} |> update
# delete
User |> get(1) |> delete
get
, get_by
, insert
, update
, delete
には!
を付けることで失敗時に例外を投げる関数になります。
phoenixではchangeset
を使ったキャスト(railsのstrong parameterみたいなの)とバリデーションを行うのでいきなりinsert
することは少ないはず。phoenixではchangeset
した結果をinsert
, update
します。
associationにinsert
railsで言うuser.article.create
user = User |> get(1)
Ecto.build_assoc(user, :articles, title: "kwsmさん", body: "わかるわ") |> insert
# 一行で
User |> get(1) |> Ecto.build_assoc(:articles, title: "kwsmさん", body: "わかるわ") |> insert
phoenixでは途中でchangeset
してinsert
する感じ?
終わり
railsでよく使っていたものを中心にまとめました。この辺りが使えれば最低限のことはできるのではないでしょうか。
preload
したものはモデルの中に保持されるので、以後データベースへのクエリは発生しません。
逆にEcto.assoc
は実行される度にデータベースへのクエリが発生します。
associationを何度も使うならば変数に入れておくか、preload
しましょう。