Help us understand the problem. What is going on with this article?

Rails使いがElixirのEctoを勉強した時のまとめ

More than 3 years have passed since last update.

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でプロジェクトを作成。

依存にectopostgrexを追加

mix.exs
  defp deps do
    [
      {:ecto, "~> 2.0"},
      {:postgrex, "0.11.2"}
    ]
  end

依存解決

$ mix deps.get

mix.exsのapplicationにpostgrex追加

mix.exs
  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
priv/repo/migrations/xxxxxxx_create_users.exs
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
priv/repo/migraions/xxxxxxx_create_articles.exs
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

スキーマ

lib/blog/user.ex
defmodule Blog.User do
  use Ecto.Schema

  schema "users" do
    field :email, :string
    field :name, :string
    timestamps

    has_many :articles, Blog.Article
  end
end
lib/blog/article.ex
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でよく使うものはここに書いておきます。

iex.exs
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.exsimport Blog.Repoしているためです。iex以外で使う時はimport Blog.Repoするか、修飾名で呼んで下さい。

クエリ構築 & 取得

railsで言うUser.where(name: "黒澤ルビィ")

User |> where(name: "黒澤ルビィ") |> all

whereimport Ecto.Queryしているためです。

User |> where(name: "zura")でクエリを構築して、allでレコードを取得するイメージです。

クエリ構築で止めることで、合成可能なクエリを構築できます。

参考: Elixir Ectoのスキーマ定義と合成可能なクエリ - モジュールからデータベース用DSLへ

もちろんgroup_by, having, join, limit, offset, order_byなどもあります。

ちなみに、取得件数が1件の時はallの代わりにoneを使います。

Ecto2.0からのパイプラインによるクエリ構築 :tada:

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, limitimport 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{..}, ..]

preloadimport 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しましょう。

techno-tanoC
Elixir, Haskell, Ruby, Rustが好き
mixi
全ての人に心地よいつながりを
http://mixi.co.jp
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away