-> English version



るびくる&RBのRubyプログラミング大作戦!
Sequelを使えばSQLも怖くない!(パート2:概要編・後半)

このエントリーをはてなブックマークに追加

るびくる:
Rubyの自称マスコットキャラクター。
新年初売りでは気になってたアクセサリーや小物類のセールを狙う。
もちろんTwitterやLINEを使って、友人との情報交換も欠かさない。

RB(あーるびー):
解説役兼るびくるの指導役。
新年初売りでは近くの商店街にある専門店の福袋を狙う。
もちろんネット検索による、事前のリサーチも欠かさない。


ハッピーニューイヤー!
あけましておめでとうございます、今年もよろしくお願いします!

みなさんは、2012年の大みそかをすっきりした気持ちで迎えることができたでしょうか?

夜の闇よりもどんよりした気持ちで新年を迎えました。

ど、どうしたのるびくる!?

RBが前回のパート1(概要編・前半)をすごい中途半端なところで終わらせるからだよ!
サンプルコードを出したところで終わるもんだから
Sequelがどんなものなのかが気になって気になって
せっかくの箱根駅伝で、日体大の30年ぶりの優勝がちっとも頭に入ってこなかったじゃない!

るびくるはもともと箱根駅伝に興味なかったんじゃなかったっけ?
確か去年(2012年)の1月にも、みんなが駅伝の余韻冷めやらぬときに
みんなの前で「柏原選手? 誰ですかそれ?」って言い放っ

わーわー聞こえなーい!!!


1. Sequelを使ったデータ取得コードの流れを見ていこう!

さて、それじゃパート1(概要編・前半)のおさらいとして
パート1の最後で載せたコードを再掲しておこう。

まず、こっちがSequelを使わずに、データベースからデータを取得するコード。

file: sql_select_sample.rb

# encoding: utf-8
require 'mysql'

# 1. サーバー名やDB名を指定して、DBに接続する
DB = Mysql.connect('mysqlserver.example.net', 'username', 'password', 'testdb', encoding: 'utf8')

# 2. SQL文を実行して、色が「赤」であるデータを名前順に取得する
sql = 'SELECT * FROM mascots WHERE color = ? ORDER BY name'
rows = DB.query(sql, '')

# 3. 取得したデータの内容(name)を表示する
rows.each do |row|
  p row[:name] # => 'るびくる'
end

そして、こっちがSequelを使って、データベースから同じようにデータを取得するコードだね。

file: sequel_select_sample.rb

# encoding: utf-8
require 'sequel'

# 1. サーバー名やDB名を指定して、DBに接続する
DB = Sequel.connect('mysql://username:password@mysqlserver.example.net/testdb', encoding: 'utf8')

# 2. SQL文を実行して、色が「赤」であるデータを名前順に取得する
mascot_dataset = DB[:mascots]
mascot_dataset = mascot_dataset.where(:color => '').order(:name)
rows = mascot_dataset.all

# 3. 取得したデータの内容(name)を表示する
mascot_dataset.each do |row|
  p row[:name] # => 'るびくる'
end

パート1の最後でRBが言ってたのは、この2つのコードを順に比較して解説していってくれる、ってことだったよね。

OK。それじゃ、順々にコードの中身を見て行こう!


# encoding: utf-8
require 'mysql'

# 1. サーバー名やDB名を指定して、DBに接続する
DB = Mysql.connect('mysqlserver.example.net', 'username', 'password', 'testdb', encoding: 'utf8')
# encoding: utf-8
require 'sequel'

# 1. サーバー名やDB名を指定して、DBに接続する
DB = Sequel.connect('mysql://username:password@mysqlserver.example.net/testdb', encoding: 'utf8')

まずは最初の、データベース(MySQL)への接続部分を見てみようか。

必要なライブラリをrequireで読み込んで、サーバー名やDB名を指定して接続、だね。
ここまでの流れは、どっちのコードもほとんど同じ?
Sequelの側では、接続にURLぽい文字列を使ってるって違いはあるけど……

そうだね。ここまではSequelを使っても、使わなくてもほとんど同じだよ。

ただ、もう一つだけ付け加えておくと、Sequelの側ではそのURLぽい文字列を書き換えることで
MySQL以外にPostgreSQLやSQL Serverなど、他の種類のデータベースにも
ほとんど同じようなコードで接続できるっていう特徴がある。

たとえばこんな風に書き換えれば、それだけでSQLiteに接続することができるんだよ。

# encoding: utf-8
require 'sequel'

# 1. データベースのパスを指定して、SQLite DBに接続する
DB = Sequel.connect('sqlite://testdb.db')

ADO.NETなんかと同じように、接続設定を書き変えて、それぞれのデータベース用のドライバを組み込むだけで
Sequelひとつで接続できる、ってこと?

そういうこと。
今回はあんまり深入りはしないけど、Sequelの利点の一つだから、ちょっと覚えておいてね。

あとからMySQLを捨ててPostgreSQLに移行したいときも大丈夫! ってことだね。

あのるびくるさん、一応これWeb上で公開されてる記事なんで、発言には少し気を使っていただけますか?


# 2. SQL文を実行して、色が「赤」であるデータを名前順に取得する
sql = 'SELECT * FROM mascots WHERE color = ? ORDER BY name'
rows = DB.query(sql, '')
# 2. SQL文を実行して、色が「赤」であるデータを名前順に取得する
mascot_dataset = DB[:mascots]
mascot_dataset = mascot_dataset.where(:color => '').order(:name)
rows = mascot_dataset.all

さて、今回のメインはここだ。データベースの「mascots」テーブルからデータを取得するコード。

Sequelを使わない側のコードは、普通にSQL文を実行してデータをとってくる、っていうよくあるコードだよね。

そうだね。
SQL SELECT文を実行した結果が、配列(Arrayオブジェクト)として取得できるから
その配列を変数「rows」に格納してる、っていうコードだね。

で、Sequelを使う側のコードが……
まずDB[:mascots]って書いてあるけど、これは何をやってるの?
たぶん上のSQL文と同じように、mascotsテーブルのデータを取得する準備をしてるのかな、ってとこまでは
何となくわかるんだけど。

ここでは、mascotsテーブルからデータを取得するために必要な
データセット(Sequel::Datasetクラスのインスタンス)を取得してるんだよ。

データセット? ……って何?

うん、それなんだけど、このデータセットは今回の話の中でもいちばん大きな話題になる。
だからもう少し後で説明させてもらうよ。

今のところは、「mascotsテーブルから取得したデータのカタマリ」みたいなものだと考えてくれればいい。

はーい。


# 3. 取得したデータの内容(name)を表示する
rows.each do |row|
  p row[:name] # => 'るびくる'
end
# 3. 取得したデータの内容(name)を表示する
mascot_dataset.each do |row|
  p row[:name] # => 'るびくる'
end

最後に、さっきデータベースから取得したデータ全件の
名前(name列の値)を表示するのが、この部分のコード。

この部分のコードは、Sequelを使っても使わなくても、まったく同じなんだね。

うん、そうだね。

とすると……Sequelを使った時の一番大きな違いは、さっきのデータセットを使うっていうことなの?
そうだとすれば、データセットを使うことで、SQL文をそのまま使ったときと比べて、いったい何が変わるの?

OK。それじゃ、これからSequelのデータセットがどんな役割を持っていて
それを使うとどんな風に嬉しいのか、ということを
順々に説明していこう。


2. データセットの役割って?

まずデータセットとは何なのか、について改めて説明しようか。
データセットっていうのは、1つのSQLクエリ(問い合わせ)をオブジェクトとして表したものなんだ。

SQLクエリをオブジェクトとして表したもの……?

この言い方だと分かりにくいかもしれないけど
要するに、 1つのデータセット = 1つのSQL文 なんだと思ってもらえばいいよ。

たとえば、以下のSQL文とデータセットの比較を見てもらえば、なんとなくわかってもらえるんじゃないかな?

file: dataset_sample.rb

# encoding: utf-8
# Sequelを使って、SQLiteデータベース「test.db」へ接続
require 'sequel'
DB = Sequel.connect('sqlite://test.db')

# 表示
p DB       #=> #<Sequel::SQLite::Database: "sqlite://test.db">


# A. mascotsテーブルからデータを取得する、SQL文の文字列(Stringオブジェクト)
mascot_sql = 'SELECT * FROM mascots'

# B. mascotsテーブルからデータを取得する、データセット(Datasetオブジェクト)
mascot_dataset = DB[:mascots]
mascot_dataset = DB.from(:mascots)  # DB[:mascots] の別記法

# 表示
p mascot_sql          #=> "SELECT * FROM mascots"
p mascot_dataset      #=> #<Sequel::SQLite::Dataset: "SELECT * FROM `mascots`">
p mascot_dataset.sql  #=> "SELECT * FROM `mascots`"

Sequelでは、Sequel.connectメソッドを使ってデータベースに接続することで
データベースオブジェクト(Databaseクラスのインスタンス)が得られる。

で、その取得したデータベースオブジェクト(DB)に対して
DB[:table_name]DB.from(:table_name) のような形でメソッドを呼び出すことで
「指定したテーブルからデータを取得するためのデータセット」を作ることができるんだ。

ふむふむ、なるほど。
この例では、DB[:mascots] ってやってるから……
mascotsテーブルからデータを取得するためのデータセット、つまり
SELECT * FROM `mascots` っていうSQL文と同じ意味のデータセットを作ってる、ってことなんだね。

そういうこと。

作ったデータセットの中身が、実際にどんなSQL文になるのかは
上のコードの例みたいに、pメソッド(もしくはDataset#inspectメソッド)を使えば、一目瞭然だよね?
代わりにDataset#sqlメソッドを使うことで、SQL文の文字列を取得することもできるし。

で、実際にこのデータセットを元にして、データベースからデータをとってくるにはどうすればいいの?

最初のコードで出てた例と同じで、Dataset#allメソッドを使えばOKだよ。

# データセットを作る。まだこの時点ではSQL文を作ってるだけで、実際のデータは取得していない!
mascot_dataset = DB[:mascots]

# データセットを元に、データベースから実際のデータを取得する
rows = mascot_dataset.all

# 表示
p rows             # 取得した全レコードの配列 (Array)
p rows.first       # 取得したレコードのうち、1行目の各列のデータを格納したHash

puts rows.first[:id]     #=> 1            (1行目レコード・id列の値)
puts rows.first[:name]   #=> るびくる     (1行目レコード・name列の値)

もしくは、その代わりにDataset#eachメソッドを使って、直接1レコードごとの処理をしてもいい。
ね。

# データセットを作る。まだこの時点ではSQL文を作ってるだけで、実際のデータは取得していない!
mascot_dataset = DB[:mascots]

# データセットを元に、データベースから実際のデータを1件ずつ取得しながら処理
mascot_dataset.each do |row|
  puts row[:id]     #=> 1            (レコード・id列の値)
  puts row[:name]   #=> るびくる     (レコード・name列の値)
end

データセットは、作った時点では単なるSQLクエリを表すオブジェクトでしかないから、まだデータの取得はしない。
alleachなどのメソッドを実行して、実際に中身のデータが必要になった時点で
SQL文を実行して、実際のデータの取得を行うんだ。

へー、「データセット」っていうと、いかにも何かのデータが入ってそうな名前だけど
実際にはデータセットの中にデータそのものが入ってるわけじゃないんだね。

そうだね。データの入れ物があるだけ、みたいなイメージかな。


あと、さっきの話を聞いてて思ったんだけど……
データセットからSQL文に変換できるってことは、逆にSQL文から直接データセットを作ったりすることもできる?

もちろんできるよ。DB['sql'] 形式の書き方で、データセットを作ればいい。

# SQL文からデータセットを作る
red = ''
raw_sql_dataset = DB['SELECT * FROM mascots WHERE color = ?', red]
   
rows = raw_sql_dataset.all

もしくは、データセットを作らずに、直接SQLを実行することもできるよ。

# SQL文を直接実行してデータ取得
red = ''
DB.fetch('SELECT * FROM mascots WHERE color = ?', red) do |row|
  puts row[:id]     #=> 1            (レコード・id列の値)
  puts row[:name]   #=> るびくる     (レコード・name列の値)
end

# データを取得しないSQL文の場合は、fetchメソッドの代わりにrunメソッドを使う
DB.run "CREATE TABLE users (name VARCHAR(255) NOT NULL, age INT(3) NOT NULL)"

なるほど、普段はデータセットをメインで使いつつ
どうしてもSQL文を直接実行したい場合だけ、DB['sql'] 形式の書き方や
fetchrunなどのメソッドを使えばいいんだね。

そういうことだね。


3. データセットを使うと、どういう利点があるの?

でもさ、今の説明を聞いてて思ったんだけど……
データセットを使ったときって、「生のSQL文の文字列」を使ったときと何が違うの?

というと?

たとえばさ、こういう2つのオブジェクトがあるとするじゃない。

# A. mascotsテーブルからデータを取得する、SQL文の文字列(Stringオブジェクト)
mascot_sql = 'SELECT * FROM mascots'

# B. mascotsテーブルからデータを取得する、データセット(Datasetオブジェクト)
mascot_dataset = DB[:mascots]          

RBの話だと、Aのmascot_sqlとBのmascot_dataset
おおむね同じ意味のオブジェクトだってことなんだよね?

そうだね。「mascotsテーブルからデータを取得するためのオブジェクト」ってところは同じだね。

そうすると、そのデータセット?を使ってみたところで
SQL文で直接書くのと、実用上あんまり変わらないんじゃないか、と思うんだけど……

ああなるほど、SQL文をそのまま実行することができるSequelで
あえてSQL文の代わりにデータセットを使う利点はなんなんだろう、ってことだね。

そうそう、そういうこと!

いや、わたしみたいにSQL文を文字列で書くのがイヤでイヤでたまらなくて
「できる限りRubyのコードだけでデータを取得したい!」っていう人の場合は
それだけでも十分だよ? データセットすごい素敵だと思うよ? 結婚したいよ?
でもそうでない人が、Sequelを使う場合に、わざわざそのデータセットを使おうとするのかなー、って。

そうだね、るびくるの疑問ももっともだ。
実際に「DBに接続してSQL文を使う」ためだけにSequelを使うっていうやり方もあるし、それだけでもSequelはそこそこ役に立つ。

でも、データセットには「SQL文を文字列で書かなくて済む」以外にも、ちゃんと意味があるんだよ。

ほかにもまだ、データセットならではの利点があるってことだね?

その通り。具体的にはこの3つだ。

  1. 使うデータベースの種類が変わっても、データ取得部分のコードの違いが少ない(DB間の差異の吸収)
  2. 部分的なクエリを組み合わせて、1つの大きなクエリを組み立てることができる(クエリの構造化や再利用がしやすい)
  3. O/Rマッパーとの相性がいい

お、O/Rマッパー……? って何?

Sequelが持ってる大きな機能の1つで、ざっくり言うと
「データベースのテーブル定義から自動的にデータクラスを作ってくれる機能」のこと。

これなしにSequelは語れない! ってくらい、すごく便利な機能なんだけど……
これについて話そうとすると、たぶん記事1回分くらいは丸ごと費やすことになりそうだし
3の利点については、とりあえず置いておこう。

わかりました先生。
その重要なところを次にとっておいて読者の期待を煽ろうと手法、さすがですね先生。

そんな週刊連載みたいな手法使わないよ! 前回パート1が中途半端なところで終わったの、まだ根に持ってるの!?


で、1つ目の利点の
「使うデータベースの種類が変わっても、データ取得部分のコードの違いが少ない(DB間の差異の吸収)」
ってことだけど……これはたとえば、MySQLでもPostgreSQLでもSQLiteでもSQL ServerでもOracleでも
ほぼ同じような感じでデータが取得できる、ってこと?

そういうこと。
データベースの種類が違うと、SQLの書き方(文法)も微妙に違ったりするよね?
データを上位20件だけ取得したいときに、ふつうはLIMIT句を使うけど、SQL ServerではTOP句じゃないとダメとか
引用符を使うとき、ふつうは二重引用符だけど、SQLiteやMySQLではだったりとか。

データセットを使えば、もうそうしたSQLの種類の違いに悩まされる夜を送らなくてもいいってこと!?

いや、残念ながらそこまでじゃない。
Sequelを使ったからといって、SQLiteで魔法のようにOVER句や再帰クエリが使えるようになるわけじゃないしね。

せいぜい、さっき挙げたようなちょっとした違いを吸収してくれるくらいかな。

なーんだ、残念。
でも、それだけでもすごくありがたいよね!
Sequelさんがデータベース間の違いを、少しでも吸収してくれれば
わたしがデータベースやSQL文から脳に受ける深刻なダメージもわずかなりと減っていくわけだし。

……るびくる、いったいデータベースにどんな思い出があるの?


じゃあ、3つの利点のうち最後に残った利点は、2つ目の
「部分的なクエリを組み合わせて、1つの大きなクエリを組み立てることができる(クエリの構造化や再利用がしやすい)」
ってところだね。
これってどういう利点なのか、正直ピンと来ないんだけど……

なるほど、確かに今までに見てきたようなサンプルコードだと
単純なクエリばかりだったから、まだピンと来ないかもしれないね。

それじゃるびくる、1つサンプルコードを書いてもらおう。
「mascotsテーブルから、色が赤で、性別が女性で、かつ体重が50.0以上であるデータを
名前順に先頭3件だけ取得する」
SQL文を返すためのメソッド定義を、ちょっとためしに書いてみてもらえる?

さらりとわたしの体重に関する重大なヒントを漏らさないでくれる!?

def red_girl_mascot_sql
  select_sql = <<SQL
SELECT * FROM mascots
WHERE color = '赤' AND sex = '女性' AND weight >= 50.0
ORDER BY name ASC
LIMIT 3

SQL

  return select_sql
end

こんな感じのSQL文でいい?
SQL恐怖症のわたしにしては、かなり頑張ったほうだと思うんだけど。

ありがとう。
それじゃ、これをデータセットを使うように書き換えた場合、どうなるかなんだけど
こんな感じのコードになる。

def red_girl_mascot_dataset
  ds = DB[:mascots]
  ds = ds.where(:color => '', :sex => '女性')
  ds = ds.where{ weight >= 50.0 }
  ds = ds.order(:name).limit(3)
  
  return ds
end

なるほど、このwhereっていうメソッドで、データセットのSQLに絞り込み条件を追加したり
orderっていうメソッドで順序を指定したりするんだね。
こっちの書き方だと、だいぶ「生のSQL文っぽさ」が薄れて、わたしにやさしい感じになってるね!

とはいえ、この時点ではまだSQL文とデータセットの間に、大きな違いはないよね。

さてるびくる。ここでもう1つ、別のメソッドを作ってほしい。
るびくるが作った、red_girl_mascot_sqlメソッドを使って取得したSQL文に対して
さらに「服の色が白である」っていう条件を追加する
add_white_cloth_conditionメソッドを実装してもらえる?

わかった!

えーと、別のメソッドで取得したSQL文に、新しく条件を追加するんだから……
メソッドの定義に、SQL文を受け取るための引数を追加する必要があるよね。

def add_white_cloth_condition(mascot_sql_string)
end

あとはこのメソッドの、処理の中身を実装して…………あ!!

気がついた?

そっか、SQL文の文字列は、もうLIMIT句まで含めてできあがってるわけだから……
そのSQL文の中に、あとから新しく絞り込み条件を足すことなんてできない!

def add_white_cloth_condition(mascot_sql_string)
  cond_sql = "AND cloth_color = '白'"

  # mascot_sql_stringに対して、どうやれば上のcond_sqlを追加できる?
end

そう。SQLクエリを、単に文字列で組み立てていこうとすると
どうしても「あとからSQL文に対して、要素(WHERE句など)の追加・変更を行うのが難しい」っていう
文字列であるがゆえの制限が付きまとう。

一応、正規表現とかでゴリゴリ文字列処理をやれば、不可能ではないかもしれないけど
そんなことは誰もやりたくないよね。

考えるだけで脳がショートしそうです。

そこでデータセットの出番なんだ。
Sequelのデータセットのように、文字列ではなく別の「SQLクエリを表すオブジェクト」を使うようにすれば
さっき挙げたような問題は解決できる。
あとから新しく絞り込み条件を足すことだって自由自在だ!

file: dataset_condition_adding.rb

# encoding: utf-8
require 'sequel'

# SQLiteデータベースをメモリ内に作成して、そのDBに接続
DB = Sequel.sqlite

def red_girl_mascot_dataset
  ds = DB[:mascots]
  ds = ds.where(:color => '', :sex => '女性')
  ds = ds.where{ weight >= 50.0 }
  ds = ds.order(:name).limit(3)
  
  return ds
end

# 文字列ではなく、データセットを受け取るように変更
def add_white_cloth_condition(mascot_dataset)
  ds = mascot_dataset.where(:cloth_color => '')
  return ds
end

# メソッドの実行サンプル
ds = red_girl_mascot_dataset        # マスコットを検索するデータセット取得
puts ds.sql   #=> "SELECT * FROM `mascots` WHERE ((`color` = '赤') AND (`sex` = '女性') AND (`weight` >= 50.0)) ORDER BY `name` LIMIT 3"

ds = add_white_cloth_condition(ds)  # そのデータセットに対して、さらに条件を追加!
puts ds.sql   #=> "SELECT * FROM `mascots` WHERE ((`color` = '赤') AND (`sex` = '女性') AND (`weight` >= 50.0) AND (`cloth_color` = '白')) ORDER BY `name` LIMIT 3"

すごい! スムーズに絞り込み条件を追加できてる!

そっか、データセットを使うようにすることで、まるでプラモデルを順々に組み上げていくように
SQLクエリを部品から少しずつ組み立てていくことができるようになるんだ!
それが「クエリの構造化や再利用がしやすい」ってことにつながるんだね!

その通り! これこそがデータセット、ひいてはSequelの大きな強みなんだ。

ちなみにこの辺の利点については
slideshareにある、桑田誠さんの「SQL上級者こそ知って欲しい、なぜO/Rマッパーが重要か?」っていうスライドで
詳しく説明されているから、興味がある人は、あわせてそっちも読んでみてね。


4. まとめ:Sequelは何をしてくれるもの?

さて、ここまでSequelの概要について説明してきたんだけど……
概要編が思ったよりも長くなっちゃったことだし
「Sequelとは結局のところ何なのか」っていう全体像について一度まとめ直しておこうか。

はーい、お願いします!

まとめると、Sequelはこの4つの役割をしてくれるデータベース・ツールキット(ライブラリ)なんだ。

  1. さまざまな種類のデータベースに接続して、どの種類のデータベースでも同じようにデータの取得や保存ができるようにする(DB間の差異の吸収)
  2. SQLクエリをRubyのオブジェクトとメソッドの組み合わせで、順々に組み立てることができる(データセット)
  3. データベースのテーブル定義を読み取って、自動的にデータクラスを生成することができる(O/Rマッパー)
  4. データベース定義をバージョン管理して、好きな時に任意のバージョンに戻すことができる(マイグレーション)

あれ!? 役割4にさりげなく聞いたことのない単語が出てます! 先生!
マイグレーションって何者!?

これもO/Rマッパーと同じで、重要な機能なんだけど、やっぱり今回のうちに話せる規模じゃないのでパス。

これも次回以降!? タイトルが「概要編」ってなってるのに、まだ4つある役割のうち2つまでしか説明されてな

それではみなさん、2013年もよろしくお願いします!

先生ーーー!! 「概要編」っていったいなんだったんですか先生ーーー!!!

パート3:基本的な使い方編に続きます。

このエントリーをはてなブックマークに追加

参考Webページ:
Sequel: The Database Toolkit for Ruby(公式サイト。英語)
Sequel READMEの日本語訳というかみじんこ訳 - uzuki05のブログ (uzuki05さんによる、公式READMEの日本語訳)
注意事項
  • 本記事の内容は、Sequel 3.41.0 時点での動作を元にしています。
  • 本記事内で使用しているアイコン画像は、桜去ほとりさんが制作されたものです。クリエイティブ・コモンズ 表示-非営利 3.0ライセンスのもとで、再配布や変更などを行っていただくことができます。
  • 本記事内の文章やソースコードは、CC0ライセンスのもとで、ご自由に利用していただくことができます。
  • ツンツク・モモコのお買い物大作戦と形式が類似していますが、本記事側が一方的に参考にさせていただいただけであり、とくに関連性はありません。

一言メッセージフォーム

るびくるへの質問や、やってほしい企画のリクエスト、Webサイト管理スタッフへの感想・意見・質問など、なんでもお気軽にどうぞ。