Drew Olsonさんの2015年1月23日付のブログ記事Composable Queries with Ectoの翻訳です。
EctoはRuby and RailsだとActive Recordに相当するのでしょうか、データベースの抽象化機構です。
Phoenixにも欠かせない機能なんですがあまり紹介記事がなさそうなので訳してみました。
そういえばectoっていうのは「外の…」という意味の接頭語で(ex-なんかと同じ)~魂?が口から「外に出る」エクトプラズム(ectoplasm)なんてのがありましたね(笑)~データベースに直接タッチせず外から触るからついた名前かもしれません。
(どうでもいい追記) ゴーストバスターズの「作業車」の名前がEcto-1でした。
以前の投稿で(ある種ホンモノの)WebアプリをElixirで作成して1今までに私が習ってきたレッスンのいくつかについて簡潔に述べました。今日はEctoを使った組み立て可能なクエリについて詳細を見てみようと思います。まずはEctoの簡単な紹介から。
Ectoとは何か
Ecto2 は軽量ORMと考えられます。EctoはElixirのstructを使ってデータベースのテーブルを表現し、データベースのクエリの構築と実行のためのDSLを提供します。つまらない著者なもんで申し訳ないんですが、使い古された「たくさんのコメントを持つ投稿」という例をEctoの能力を示すために使うことにしましょう。次のようなモデルを仮定します。
defmodule MyApp.Post do
use Ecto.Model
import Ecto.Query
schema "posts" do
field :body, :string
field :published, :boolean
field :published_at, :datetime
field :title, :string
has_many :comments, MyApp.Comment
end
end
defmodule MyApp.Comment do
use Ecto.Model
import Ecto.Query
schema "comments" do
field :commenter, :string
field :title, :string
field :votes, :integer
belongs_to :post, MyApp.Post
end
end
Ectoでクエリを
Ectoは2通りのクエリシンタックスを提供します:キーワードクエリシンタックスとクエリ式です。どちらのクエリスタイルでもクエリの構成はクエリの実行とは分離されたプロセスであることに注意してください。それぞれのスタイルで構成されたクエリはアプリケーションのRepo
に渡されて実行されます。
キーワードクエリシンタックス
キーワードクエリシンタックスはSQLを密接に反映した、LINQを彷彿させるスタイルです。いくつかの例題クエリで説明しましょう。
すべての投稿を選択
MyApp.Repo.all(
from p in MyApp.Post,
select: p
)
全ての公開済みの投稿を選択
MyApp.Repo.all(
from p in MyApp.Post,
where: p.published == true,
select: p
)
1番めの投稿の全てのコメントを選択
MyApp.Repo.all(
from c in MyApp.Comment,
join: p in assoc(c, :post),
where: p.id == 1,
select: c
)
クエリ式
クエリ式はElixirのAPIでよく見られるパイプラインのコンセプトに従っています。これらのクエリ全てはモデルモジュール(例えばMyApp.Post
)から始まっているという点は重要です。モデルそのものがクエリになれるオブジェクトであり与えられたテーブルの全アイテムを表現しています。
すべての投稿を選択
MyApp.Post |> MyApp.Repo.all
全ての公開済みの投稿を選択
MyApp.Post
|> where([p], p.published == true)
|> MyApp.Repo.all
1番めの投稿の全てのコメントを選択
MyApp.Comment
|> join(:left, [c], p in assoc(c, :post))
|> where([_, p], p.id == 1)
|> select([c, _], c)
|> MyApp.Repo.all
クエリの組み立て
クエリ式スタイルでどのようにクエリを組み立てるかについては見ればわかりますね…新しい制約をパイプラインに追加するだけです。キーワードクエリシンタックスで作られたクエリや異なるスタイルのクエリからクエリを組み立てるやり方はそれほど明白ではありません。
まず、キーワードクエリシンタックスの重要な仕様について理解する必要があります。from
節でin
以降のトークンにはクエリ可能オブジェクトなら何でも置けて、かつ、他のクエリも「クエリ可能オブジェクト」なのです! 例を示します。
query = from p in MyApp.Post,
select: p
query2 = from p in query,
where: p.published == true
MyApp.Repo.all(query2)
これを踏まえると、2つの文法スタイルを混ぜてくっつけることができます。
query = from p in MyApp.Post,
select: p
query |> where([p], p.published == true) |> MyApp.Repo.all
ぜんぶまとめると
では我々のEctoモデルにいい感じの、中身がわかるような名前の関数をいくつか追加しましょう。
defmodule MyApp.Post do
use Ecto.Model
import Ecto.Query
schema "posts" do
field :body, :string
field :published, :boolean
field :published_at, :datetime
field :title, :string
has_many :comments, MyApp.Comment
end
def published(query) do
from p in query,
where: p.published == true
end
def sorted(query) do
from p in query,
order_by: [desc: p.published_at]
end
end
defmodule MyApp.Comment do
use Ecto.Model
import Ecto.Query
schema "comments" do
field :commenter, :string
field :title, :string
field :votes, :integer
belongs_to :post, MyApp.Post
end
def for_post(query, post) do
from c in query,
join: p in assoc(c, :post)
where: p.id == ^post.id
end
def popular(query) do
query |> where([c], c.votes > 10)
end
end
クエリの組み立てにおいてクエリスタイルがお互いに交換可能であることを示すために両方のクエリスタイルを使っています。では追加した関数でいくつかクエリを作ってみましょう。通常、私はこのタイプのクエリの組み立てをPhoenixで使います。
alias MyApp.Post # 訳注: alias で引数なしだと以下PostがMyApp.Postと同じ意味になります。次も同様。
alias MyApp.Comment
published_posts = Post # (訳注: 公開された投稿)
|> Post.published
|> MyApp.Repo.all
last_post = Post # (訳注:最後の投稿)
|> Post.published
|> Post.sorted
|> MyApp.Repo.one
recent_popular_comments = Comment # (訳注:最新の人気のあるコメント)
|> Comment.for_post(last_post)
|> Comment.popular
|> MyApp.Repo.all
まとめ
この記事を通じてソースから取り出して再利用可能なクエリコンポーネントとそれを組み立ててもっと複雑なクエリを作れるEctoのパワーと柔軟性について何らかの見識を持っていただければ幸いです。これらのテクニックを使うとテストを支援する際にも重複や複雑さを減らすことができますよ。