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`