DataMapperのクエリチェインが使えない(場合の)話
dm-core-1.2.0の話です。
DataMapperのクエリチェイン(正式名称しらない。。)は特定の条件下(DataMapper::Collection#union(|, +))で利用できない理由と、回避策を調べた。 長いです。
class User include DataMapper::Resource property :id, Serial property :name, String property :active, Boolean def self.active all(active: true) end def self.name_start_with(prefix) all(:name.like => "#{prefix}%") end end # DataMapperはこんなことができる(クエリチェイン) User.active.name_start_with('a') # SELECT "id", "name", "active" # FROM "users" # WHERE ("active" = 't' AND "name" LIKE 'a%') # ORDER BY "id" # orを使いたい場合はこんな感じ a = (User.name_start_with('a') | User.name_start_with('b')) # SELECT "id", "name", "active" # FROM "users" # WHERE ("name" LIKE 'a%' OR "name" LIKE 'b%') # ORDER BY "id" # likeの条件は維持される # が、調子に乗ると、、 b = (User.name_start_with('a') | User.name_start_with('b')).active # SELECT "id", "name", "active" # FROM "users" # WHERE "active" = 't' # ORDER BY "id" # likeの条件が落ちる!! c = (User.name_start_with('a') | User.name_start_with('b')).all(active: true) # SELECT "id", "name", "active" # FROM "users" # WHERE (("name" LIKE 'a%' OR "name" LIKE 'b%') AND "active" = 't') # ORDER BY "id" # likeの条件は維持される d = (User.name_start_with('a').active | User.name_start_with('a').active) # SELECT "id", "name", "active" # FROM "users" # WHERE (("name" LIKE 'a%' OR "name" LIKE 'b%') AND "active" = 't') # ORDER BY "id" # likeの条件は維持される actives = User.active e = (actives.name_start_with('a') | actives.name_start_with('b')) # SELECT "id", "name", "active" # FROM "users" # WHERE (("active" = 't' AND "name" LIKE 'a%') OR # ("active" = 't' AND "name" LIKE 'b%')) # ORDER BY "id" # likeの条件は維持される
なんで?
まずactiveを呼び出す処理を確認する。 activeはUserに定義しているのでDataMapper::Collectionには存在しない。DataMapper::Collection#method_missingでModelクラスに処理を移譲している。
def method_missing(method, *args, &block) relationships = self.relationships if model.respond_to?(method) delegate_to_model(method, *args, &block) elsif relationship = relationships[method] || relationships[DataMapper::Inflector.singularize(method.to_s).to_sym] delegate_to_relationship(relationship, *args) else super end end def delegate_to_model(method, *args, &block) model = self.model model.send(:with_scope, query) do model.send(method, *args, &block) end end
DataMapper::Model#with_scopeはDataMapper::Model::Scopeに定義されていて、実装は次の通り。
def with_scope(query) options = if query.kind_of?(Hash) query else query.options end # merge the current scope with the passed in query with_exclusive_scope(self.query.merge(options)) { |*block_args| yield(*block_args) } end
with_scopeに渡されるqueryは次の結果である。
(User.name_start_with('a') | User.name_start_with('b')).query
このため、クエリチェインを利用した場合、次のquery.optionsしか利用されない。
(User.name_start_with('a') | User.name_start_with('b')).query.options
次に(a),(b),(c)のケースについてDataMapper::Collectionのqueryとquery.optionsの中身を確認してみる。
a = (User.name_start_with('a') | User.name_start_with('b')).query # => #<DataMapper::Query # @repository=:default # @model=User # @fields=[ # #<DataMapper::Property::Serial @model=User @name=:id>, # #<DataMapper::Property::String @model=User @name=:name>, # #<DataMapper::Property::Boolean @model=User @name=:active> # ] # @links=[] # @conditions=(name LIKE "a%" OR name LIKE "b%") # @order=[ # #<DataMapper::Query::Direction @target=#<DataMapper::Property::Serial @model=User @name=:id> @operator=:asc> # ] # @limit=nil # @offset=0 # @reload=false # @unique=false # > a = (User.name_start_with('a') | User.name_start_with('b')).query.options # => {:fields=> # [#<DataMapper::Property::Serial @model=User @name=:id>, # #<DataMapper::Property::String @model=User @name=:name>, # #<DataMapper::Property::Boolean @model=User @name=:active>], # :order=> # [#<DataMapper::Query::Direction @target=#<DataMapper::Property::Serial @model=User @name=:id> @operator=:asc>], # :unique=>false, # :add_reversed=>false, # :reload=>false} b = (User.name_start_with('a') | User.name_start_with('b')).active.query # => #<DataMapper::Query # @repository=:default # @model=User # @fields=[ # #<DataMapper::Property::Serial @model=User @name=:id>, # #<DataMapper::Property::String @model=User @name=:name>, # #<DataMapper::Property::Boolean @model=User @name=:active> # ] # @links=[] # @conditions=(active = true) # @order=[ # #<DataMapper::Query::Direction @target=#<DataMapper::Property::Serial @model=User @name=:id> @operator=:asc> # ] # @limit=nil # @offset=0 # @reload=false # @unique=false # > b = (User.name_start_with('a') | User.name_start_with('b')).active.query.options # {:fields=> # [#<DataMapper::Property::Serial @model=User @name=:id>, # #<DataMapper::Property::String @model=User @name=:name>, # #<DataMapper::Property::Boolean @model=User @name=:active>], # :order=> # [#<DataMapper::Query::Direction @target=#<DataMapper::Property::Serial @model=User @name=:id> @operator=:asc>], # :unique=>false, # :add_reversed=>false, # :reload=>false, # :active=>true} c = (User.name_start_with('a') | User.name_start_with('b')).all(active: true).query # => #<DataMapper::Query # @repository=:default # @model=User # @fields=[ # #<DataMapper::Property::Serial @model=User @name=:id>, # #<DataMapper::Property::String @model=User @name=:name>, # #<DataMapper::Property::Boolean @model=User @name=:active> # ] # @links=[] # @conditions=((name LIKE "a%" OR name LIKE "b%") AND active = true) # @order=[ # #<DataMapper::Query::Direction @target=#<DataMapper::Property::Serial @model=User @name=:id> @operator=:asc> # ] # @limit=nil # @offset=0 # @reload=false # @unique=false # > c = (User.name_start_with('a') | User.name_start_with('b')).all(active: true).query.options # {:fields=> # [#<DataMapper::Property::Serial @model=User @name=:id>, # #<DataMapper::Property::String @model=User @name=:name>, # #<DataMapper::Property::Boolean @model=User @name=:active>], # :order=> # [#<DataMapper::Query::Direction @target=#<DataMapper::Property::Serial @model=User @name=:id> @operator=:asc>], # :unique=>false, # :add_reversed=>false, # :reload=>false, # :active=>true
(b)と(c)で等価なqueryになるべきだが、@conditionsの中身が異なることがわかる。 また、(a)、(b)、(c)のどのquery.optionsにもorの条件が含まれないことがわかる。
という事で、query.optionsにorの条件が含まれないため、orを含むクエリの場合、条件が壊れてしまうことがわかった。
回避策
クエリチェイン使わなければ、意図したSQLになる。
((User.name_start_with('a') | User.name_start_with('b')) & User.active) # SELECT "id", "name", "active" # FROM "users" # WHERE (("name" LIKE 'a%' OR "name" LIKE 'b%') AND "active" = 't') # ORDER BY "id"
結論
一度、DataMapper::Collection#union(|, +)を利用すると、それ以降クエリチェインは(クエリが壊れるため)利用できない。
クエリチェインは利用できないがDataMapper::Collection#intersection(&)を利用することで回避できる。
# これは使えない。 (User.name_start_with('a') | User.name_start_with('b')).active # こっちはOK ((User.name_start_with('a') | User.name_start_with('b')) & User.active) # これもOK (allは使える) (User.name_start_with('a') | User.name_start_with('b')).all(active: true) # allに渡す条件次第ではこれも使える(冗長な書き方) (User.name_start_with('a') | User.name_start_with('b')).all(User.active.query.options)