SequelのTipsのようなもの
Rubyにおける軽量なデータベースツールキットとして、なかなかお手軽なSequelですが、実際に使うにあたっていくつか悩んだポイントがあったので、備忘録としてその解決法をメモしておきます。
なお、この記事はRuby 1.9.1p378とSequel 3.8.0の組み合わせを対象として書いています。
モデルの定義前にDB接続をしたくない
SequelにもActiveRecordパターンに基づいたモデルの仕組みが用意されていますが、「モデルを定義した時点でデータベースへの接続が存在していなければならない」という制約があります。
つまり、
require 'sequel' Sequel.connect('sqlite://test.db') class User < Sequel::Model; end p User.all
は動きますが、
require 'sequel' class User < Sequel::Model; end # Sequel::Error Sequel.connect('sqlite://test.db') p User.all
は動きません。"No database associated with Sequel::Model"といって怒られます。
この制約がどういうときに不便かというと、例えば
require 'sequel' class App def self.connect_database(option) Sequel.connect(option) end class User < Sequel::Model; end # Sequel::Error end App.connect_database('sqlite://test.db') p App::User.all
のように、データベース関連の定義をクラスやモジュールの中に押し込めようとしたとき、ちょっと綺麗に書けなくなってしまいます。
これはmodule_evalを使うことで解決できます。
require 'sequel' class App class << self def connect_database(option) Sequel.connect(option) define_models end def define_models module_eval %{ class User < Sequel::Model; end } end end end App.connect_database('sqlite://test.db') p App::User.all
実際にはモデル定義のタイミングを遅らせているだけなのですが、とりあえず「それっぽく書きたい」という目的は達成できるかな、と。
なお、Sequel::Modelを継承したクラスは、放っておくと勝手に「それまでに接続した一番最初のデータベース」と関連付けられてしまいます。例えば複数のクラスでそれぞれ別のデータベースを参照させたい場合は、以下のようにモデルクラスの継承時にデータベースのインスタンスを渡してやればOKです。
require 'sequel' module App def connect_database(option) db = Sequel.connect(option) define_models(db) end def define_models(db) module_eval %{ class User < Sequel::Model(db); end } end end class AppA extend App end class AppB extend App end AppA.connect_database('sqlite://testa.db') AppB.connect_database('sqlite://testb.db') p AppA::User.all p AppB::User.all
あまりこういうケースがあるかどうかはわかりませんが…。
文字列型のデータをforce_encodingしたい
デフォルトでは、取得した文字列型データのエンコーディングはASCII-8BITになっています。
require 'sequel' Sequel.connect('sqlite://test.db') class User < Sequel::Model; end p User.first.name.encoding # => #<Encoding:ASCII-8BIT>
逐一String#force_encodingをかけてもいいのですが、面倒な場合はForceEncodingプラグインを使うことで、全ての文字列型カラムのエンコーディングを強制的に指定できます。
require 'sequel' Sequel.connect('sqlite://test.db') class User < Sequel::Model plugin :force_encoding, 'UTF-8' end p User.first.name.encoding # => #<Encoding:UTF-8>
ちなみに、モデルではなくデータセット経由で操作する場合は、このような全体的にエンコーディングの指定をする方法はなく、それぞれにforce_encodingをかけるしかないようです。
require 'sequel' db = Sequel.connect('sqlite://test.db') p db[:users].first[:name] # => "\xE5\xA4\xAA\xE9\x83\x8E" p db[:users].first[:name].force_encoding('UTF-8') # => "太郎"
プライマリキーを指定したい
Sequelのモデルは、デフォルトではプライマリキーはRestrictedな値として、明示的な指定が許可されていない状態になっています。
require 'sequel' db = Sequel.connect('sqlite://test.db') # データセット経由の場合は関係ない db[:users].insert(:id => 123, :name => 'John', :age => 18) # ok class User < Sequel::Model; end User.create(:id => 456, :name => 'Bob', :age => 17) # Sequel::Error # => ...method id= doesn't exist or access is restricted to it...
モデル定義時にunrestrict_primary_keyを宣言することで、プライマリキーの明示的な指定が可能となります。
require 'sequel' Sequel.connect('sqlite://test.db') class User < Sequel::Model unrestrict_primary_key end User.create(:id => 456, :name => 'Bob', :age => 17) # ok
同名のカラムが存在するテーブル同士をJOINしたい
例えばusersとpostsというテーブルがあり、双方がcreated_atという名前でレコードの生成日時を記録しているとします。このとき、
require 'sequel' Sequel.connect('sqlite://test.db') class User < Sequel::Model one_to_many :posts end class Post < Sequel::Model many_to_one :users end p Post.join(User, :id => :user_id).first.created_at # => User's created_at
のように単純にJOINすると、得られたレコードのcreated_atはUserのそれとなります。
こういうケースではgraphメソッドを使うことでカラム名の衝突を防ぐことができます。graphメソッドは、テーブル毎に個別のレコードを格納したハッシュとして結果を返します。
require 'sequel' Sequel.connect('sqlite://test.db') class User < Sequel::Model one_to_many :posts end class Post < Sequel::Model many_to_one :users end record = Post.graph(User, :id => :user_id).first p record[:posts].created_at # => Post's created_at p record[:users].created_at # => User's created_at
3つ以上のテーブルをJOINしたい
SequelはメソッドチェインでさくさくSQLを生成できるのが便利なのですが、たまにそこがハマりポイントになることがあります。
例えば今説明したgraphメソッド。これはJOINの左側のテーブルとして、「直前までに生成されたデータセットのうち、最後にJOINされたテーブル」を使用します。
つまり、以下のようなコードを実行すると、
require 'sequel' Sequel.connect('sqlite://test.db') class User < Sequel::Model one_to_many :posts end class Category < Sequel::Model one_to_many :posts end class Post < Sequel::Model many_to_one :users many_to_one :categories end p Post.graph(User, :id => :user_id) .graph(Category, :id => :category_id) .first # Sequel::DatabaseError
categoriesテーブルをJOINする段階で*1"no such column: users.category_id"と怒られます。postsとcategoriesをJOINして欲しいのに、usersとcategoriesをJOINしようとしてしまっているわけです。
このようなケースでは、:implicit_qualifierオプションによってJOINの左辺を指定してやります。
require 'sequel' Sequel.connect('sqlite://test.db') class User < Sequel::Model one_to_many :posts end class Category < Sequel::Model one_to_many :posts end class Post < Sequel::Model many_to_one :users many_to_one :categories end p Post.graph(User, :id => :user_id) .graph(Category, { :id => :category_id }, :implicit_qualifier => :posts) .first # ok
{}によって第2引数のJOIN条件と第3引数のオプションを明示的に区別することを忘れずに。
気を付けたいSequelのクセ
その他、場合によっては悩みポイントとなるかもしれない、ちょっと変わったSequelのクセについてもメモを残しておきます。
Sequel::DATABASESの存在
Sequelでは、生成したデータベースを全てSequel::DATABASESという配列に格納して保持しています。
注意したいのは、ブロック内でデータベースを扱う場合は、ブロックを抜けたタイミングでSequel::DATABASESからインスタンスを削除してくれるのに、ブロックを使わずにdisconnectメソッドで接続を切った場合は、Sequel::DATABASESからインスタンスを削除してくれず、残ったままになる…という点です。
require 'sequel' puts Sequel::DATABASES.length # => 0 Sequel.connect('sqlite://test.db') do |db| puts Sequel::DATABASES.length # => 1 end puts Sequel::DATABASES.length # => 0 db = Sequel.connect('sqlite://test.db') puts Sequel::DATABASES.length # => 1 db.disconnect puts Sequel::DATABASES.length # => 1 Sequel::DATABASES.delete(db) puts Sequel::DATABASES.length # => 0
この場合、当然参照が残ったままになるのでGCの対象になりませんし、また前述のように「デフォルトではモデルは最初に接続したデータベース*2と関連付けられる」ため、思わぬところで変な挙動をする危険性があります。*3
必要であれば、上のコードの最後のように自分でSequel::DATABASESからインスタンスを取り除いてやると良いかと思います。
モデルに定義されるアクセサメソッド
前述のように、Sequelのモデルは定義時にデータベースに接続済みである必要があります。
何故かというと、モデルの定義時にテーブルに存在するカラムを取得し、各カラムに対応するアクセサメソッドを定義しているためです。
次のようなコードを実行してみるとわかりやすいです。
require 'sequel' require 'logger' db = Sequel.connect('sqlite://test.db') db.loggers << Logger.new($stderr) class User < Sequel::Model; end # この時点でSQLを発行している # SQLiteの場合は PRAGMA table_info('users')
この場合に何に注意したら良いかというと、「モデルの定義以降に追加・変更されたカラムはアクセサメソッドによるアクセスができない」という点です。
require 'sequel' db = Sequel.connect('sqlite://test.db') class User < Sequel::Model; end puts User.instance_methods.include?(:name) # => true puts User.first.name # ok db.alter_table(:users) do add_column :country, :string, :default => 'jp' end # ハッシュによるアクセスはいつでも可能 puts User.first[:country] # => jp puts User.instance_methods.include?(:country) # => false puts User.first.country # NoMethodError
これもあまり問題になるケースは無さそうな気がしますが、万一これが問題になる場合は、alter_table後に再度モデル定義をしてやればOKです。その際はremove_constで一度モデルクラスを削除することを忘れずに。