Edited at

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