DataMapper を使う (Finding)

さて,今回は検索について.1ヶ月ぐらい前に書いたものだけど...
(この続き→ DataMapper を使う - KrdLabの不定期日記)


いつも通り以下のサイトを参考に,ざっと使い方をメモってみた.

使えるメソッド

DataMapper の検索メソッドには,get, all, first がある.

zoo   = Zoo.get(1)                        # primary key
zoo   = Zoo.get!(1)                       # 失敗すると ObjectNotFoundError

zoo   = Zoo.get('DFW')                    # natural primary key
zoo   = Zoo.get('Metro', 'DFW')           # composite key

zoo   = Zoo.first(:name => 'Luke')        # name が 'Luke' のレコードで,最初にマッチしたもの

zoos  = Zoo.all                           # 全レコード
zoos  = Zoo.all(:open => true)            # open が true になっている全レコード
zoos  = Zoo.all(:opened_on => (s..e))     # 範囲指定して,ヒットした全レコードを返す

条件指定

検索条件は Hash の形式で記述する.

exhibitions = Exhibition.all(:run_time.gt => 2, :run_time.lt => 5)
# SQL の条件は 'run_time > 2 AND run_time < 5'

使用可能なシンボルは以下の通り.

gt    # ex. 5 <  x
lt    # ex. x <  5
gte   # ex. 5 <= x
lte   # ex. x <= 5
not   # ex. x != 5
like  # ex. x like '%hoge%'

オーダ指定

:order で指定する.

zoos = Zoo.all(:order => [:count.desc])
# SELECT * FROM Zoos ORDER BY count DESC

昇順は asc,降順は desc.得られた結果を逆順にすることもできる.

reversed_zoos = zoos.reverse
# SELECT * FROM Zoos ORDER BY count ASC

conditions を使ったり,混ぜて使うこともできる.

zoos = Zoo.all(:conditions => {:id => 34})
zoos = Zoo.all(:conditions => ["id = ?", 34])
zoos = Zoo.all(:conditions => ["name LIKE ? OR abbr LIKE ?", 'foo%', '%bar'])
zoos = Zoo.all(:conditions => {:id => 34}, :name.like => '%foo%')

サンプル

とりあえず,ここまでの内容でサンプルをこさえてみた.

class Person
  include DataMapper::Resource
  property :id, Serial
  property :name, String
  property :age, Integer
end
mysql> select * from people;
+----+------+------+
| id | name | age  |
+----+------+------+
|  1 | aaa  |   10 |
|  2 | bbb  |   20 |
|  3 | ccc  |   30 |
|  4 | ddd  |   31 |
+----+------+------+
Person.get(10)
# nil

Person.get!(10)
# Could not find Person with key [10] (DataMapper::ObjectNotFoundError)

Person.all(:age.gt => 10, :age.lt => 31)
# [#<Person id=2 name="bbb" age=20>, #<Person id=3 name="ccc" age=30>]

Person.all(:order => [:age.desc])
# [#<Person id=4 name="ddd" age=31>, #<Person id=3 name="ccc" age=30>, #<Person id=2 name="bbb" age=20>, #<Person id=1 name="aaa" age=10>]

pl = Person.all(:order => [:age.desc])
pl.reverse    # こんなこともできる
# [#<Person id=1 name="aaa" age=10>, #<Person id=2 name="bbb" age=20>, #<Person id=3 name="ccc" age=30>, #<Person id=4 name="ddd" age=31>]

Person.all(:conditions => {:age.gt => 10, :age.lt => 31}, :name.like => 'c%')
# [#<Person id=3 name="ccc" age=30>]

デフォルトオーダ

デフォルトのオーダリングを指定できる.
例えば以下のように「年齢の降順」を指定すると,通常の all 呼び出しで ordering が発生する.

class Person
  include DataMapper::Resource
  property :id, Serial
  property :name, String
  property :age, Integer

  default_scope(:default).update(:order => [:age.desc])
end

Person.all
# [#<Person id=4 name="ddd" age=31>, #<Person id=3 name="ccc" age=30>, #<Person id=2 name="bbb" age=20>, #<Person id=1 name="aaa" age=10>]

Scopes and Chaining

メソッドは連続して適用できる (スコープが引き継がれる).

ps = Person.all(:age.gt => 10, :age.lt => 31)
ps = ps.all(:name.like => 'c%')
p ps

「スコープ」とは,上の例でいえば

  • 1 行目は「10 < age < 31 の範囲」にあるレコード
  • 2 行目は「10 < age < 31 の範囲」かつ「name like "c%"」であるレコード

となる.


なお,発行される SQL は以下のようになる.2 回呼び出したからといって,2 回発行されるわけではない.

SELECT `id`, `name`, `age` FROM `people` WHERE (`age` > 10) AND (`age` < 31) AND (`name` LIKE 'c%') ORDER BY `id`


メソッドとしてあらかじめスコープを定義することもできる.

class Person
  ...
  def self.upper
    all(:age.gte => 20)
  end
end


Method chaining でもスコープは引き継がれる.
例えば,

class Person
  include DataMapper::Resource
  property :id, Serial
  property :name, String
  property :age, Integer

  def self.upper
    all(:age.gte => 20)
  end
end

であったとして,レコードが以下のような構成であった場合

mysql> select * from people;
+----+------+------+
| id | name | age  |
+----+------+------+
|  1 | aaa  |   10 |
|  2 | bbb  |   19 |
|  3 | ccc1 |   30 |
|  4 | ccc2 |   31 |
|  5 | bbbb |   20 |
+----+------+------+

次のような感じで使える.

Person.upper.all(:name.like => 'b%')
# [#<Person id=5 name="bbbb" age=20>]

upper の範囲内にある 'b%' なレコードが結果として得られる.


SQL を直接実行

直接 SQL を投げることも可能.

zoos = repository(:default).adapter.query('SELECT name, open FROM zoos WHERE open = 1')

ただし,戻り値は (上の例でいくと) Zoo クラスのインスタンスではなく,name と open を持ったオブジェクトが read-only で返ってくる.


条件を指定することも可能.

zoos = repository(:default).adapter.query('SELECT name, open FROM zoos WHERE name = ?', "Awesome Zoo")


結果を返さないクエリの場合は execute を使用する.

repository(:default).adapter.execute("INSERT INTO people (name, age) VALUE ('eee', 21)")
# ↑別のテーブル使って実験していたので,ここだけ SQL が違ってる.

Counting

以下のようにする.

require 'dm-aggregates'
count = Zoo.count(:age.gt => 200)  # 条件にマッチしたレコード数が整数値で返ってくる

詳細は以下を参照.
http://datamapper.org/doku.php?id=dm-more:dm-aggregates

Associations

テーブル間をまたいだ検索.
例えば著者と書籍の間に 1 対 多 の関連をはる.

class Book
  include DataMapper::Resource
  property :id, Serial
  property :title, String
  belongs_to :author
end

class Author
  include DataMapper::Resource
  property :id, Serial
  property :name, String
  has n, :books
end

テーブル上のレコードは以下の様になっていたとしよう.

mysql> select * from authors;
+----+--------+
| id | name   |
+----+--------+
|  1 | krdlab |
+----+--------+

mysql> select * from books;
+----+-------+-----------+
| id | title | author_id |
+----+-------+-----------+
|  1 | aaa   |         1 |
|  2 | bbb   |         1 |
|  3 | ccc   |         1 |
+----+-------+-----------+

books から authors の値を条件にレコードを引っ張ってくる (JOIN して SELECT する) 場合は,以下のようにする.

Book.all('author.name' => 'krdlab')
# [#<Book id=1 title="aaa" author_id=1>, #<Book id=2 title="bbb" author_id=1>, #<Book id=3 title="ccc" author_id=1>]
# krdlab の書籍がヒット

Book.all('author.name.not' => 'krdlab')
# []
# not krdlab の書籍はなかった

Book.all('author.name' => 'krdlab', :title.like => 'a%')
# [#<Book id=1 title="aaa" author_id=1>]
# ↑
# このとき発行される SQL は以下の通り.
# SELECT `books`.`id`, `books`.`title`, `books`.`author_id` FROM `books` INNER JOIN `authors` ON (`authors`.`id` = `books`.`author_id`) WHERE (`books`.`title` LIKE 'a%') AND (`authors`.`name` = 'krdlab') ORDER BY `books`.`id`