読者です 読者をやめる 読者になる 読者になる

DataMapperでinに空の配列が渡された時になんとか空のコレクションを返す

dm-core-1.2.0の話です。

DataMapperはinの条件に配列を渡すと次のように良い感じで検索してくれる。

class User
  include DataMapper::Resource
  
  property :id, Serial
  property :name, String
end

User.all(id: [1, 2, 3])
# SELECT "id", "name"
# FROM "users"
# WHERE "id" IN (1, 2, 3)
# ORDER BY "id"

User.all(id: [])
# => [] # 不正なクエリのためDataMapperがSQLを発行せず[]を返す
User.all(id: []).query.conditions.valid?
# => false

しかし、調子に乗ると、、、

しかし、調子に乗ってDataMapper::Collection.union(|)を利用すると、次の用にクエリが壊れてしまう。

User.all(id: [1, 2])
# SELECT "id", "name"
# FROM "users"
# WHERE "id" IN (1, 2)
# ORDER BY "id"

User.all(id: []).all(name: 'hoge')
# => []
User.all(id: []).all(name: 'hoge').query.conditions.valid?
# => false

(User.all(id: [1, 2]) | User.all(id: []).all(name: 'hoge'))
# SELECT "id", "name"
# FROM "users"
# WHERE ("id" IN (1, 2) OR "name" = 'hoge')
# ORDER BY "id"
# orの条件がおかしい。

# 期待するSQLは次のはずなのに、、
# SELECT "id", "name"
# FROM "users"
# WHERE "id" IN (1, 2)
# ORDER BY "id

(User.all(id: [1, 2]) & User.all(id: []).all(name: 'hoge'))
# => []
(User.all(id: [1, 2]) & User.all(id: []).all(name: 'hoge')).query.conditions.valid?
# => false
# & だとこの不具合は発生しない。。。

ちょっとやんちゃすると壊れてしまうのは困る。

回避策

この回避方法はやっつけ仕事である。 空の配列が渡された時にまずマッチしないであろう値['NOT_EXISTS']に書き換えて回避する。

module DataMapper
  class Query
    module Conditions
      class InclusionComparison < AbstractComparison
        # inの検索のときに空の配列を渡された場合、現実的にマッチしない条件に置き換える
        def initialize(subject, value)
          @subject      = subject
          @loaded_value = typecast(value)
          if @loaded_value.is_a?(Enumerable) and @loaded_value.empty?
            @loaded_value = typecast(['NOT_EXISTS'])
          end
          @dumped_value = dump
        end
      end
    end
  end
end

(User.all(id: [1, 2]) | User.all(id: []).all(name: 'hoge'))
# SELECT "id", "name"
# FROM "users"
# WHERE ("id" IN (1, 2) OR ("id" IN ('NOT_EXISTS') AND "name" = 'hoge'))
# ORDER BY "id"

とりあえず、意図した結果が取得できるSQLになった。

まとめ

だれか、もっと良い解決方法教えてください、、、