DataMapperでinner joinする

dm-core-1.2.0の話です。

DataMapperでjoinする話が全然検索に引っかからないので。メモ的に

class User
  include DataMapper::Resource

  property :id, Serial
  property :name, String

  has 1, :profile, 'UserProfile'
end

class UserProfile
  include DataMapper::Resource

  property :id, Serial
  property :address, String

  belongs_to :user
end

# 関連として定義したUser.profileを条件にわたす
# ユーザープロフィールが存在するもののみ取得
User.all(User.profile.id.not => nil)
# SELECT "users"."id", "users"."name"
# FROM "users" INNER JOIN "user_profiles" ON "users"."id" = "user_profiles"."user_id"
# WHERE NOT("user_profiles"."id" IS NULL)
# GROUP BY "users"."id", "users"."name"
# ORDER BY "users"."id"

# ユーザープロフィールで住所が東京から始まるものだけ取得
User.all(User.profile.address.like => '東京%')
# SELECT "users"."id", "users"."name"
# FROM "users" INNER JOIN "user_profiles" ON "users"."id" = "user_profiles"."user_id"
# WHERE "user_profiles"."address" LIKE '東京%'
# GROUP BY "users"."id", "users"."name"
# ORDER BY "users"."id"

# User.profileとUser.profile.addressはDataMapper::Query::Pathオブジェクト
pp User.profile.address
# #<DataMapper::Query::Path:0x007ff21d040b48
#  @model=UserProfile,
#  @property=nil,
#  @relationships=nil,
#  @repository_name=:default>

異なるテーブルをjoinする場合は問題ないが、自分自身をjoinする場合、条件の混同が発生してしまう。

class Category
  include DataMapper::Resource
  property :id,         Serial
  property :name,       String
  property :parent_id,  Integer

  belongs_to :parent_category, 'Category',
    parent_key: [:id],
    child_key: [:parent_id]
end

Category.all(Category.parent_category.name => "parent", :name => "child")
# SELECT "categories"."id", "categories"."name", "categories"."parent_id"
# FROM "categories" INNER JOIN "categories" "categories_1" ON "categories"."parent_id" = "categories_1"."id"
# WHERE ("categories"."name" = 'parent' AND "categories"."name" = 'child')
# GROUP BY "categories"."id", "categories"."name", "categories"."parent_id"
# ORDER BY "categories"."id"

# joinしたテーブルの条件とjoinされるテーブルの条件が混同されている
# WHERE ("categories"."name" = 'parent' AND "categories"."name" = 'child')

調べてみたけど、解決方法を見つけられなかった。次のようにconditionsを利用して混同を避けたりしてる。

# exists を使ってjoinの代わりとする。
Category.all(
  name: "child",
  conditions: [
    "exists (select id from categories as C where C.id = categories.id and name = ?)",
    'parent'
  ])
# SELECT "id", "name", "parent_id"
# FROM "categories"
# WHERE ("name" = 'child' AND (exists (select id from categories as C where C.id = categories.id and name = 'parent')))
# ORDER BY "id"

DBがレガシーで次のように:fieldを指定している場合、その定義を再利用すると良いかもしれない。

class Category
  property :name, String, field: 'CAT_NAME'
end

Category.properties[:name].field
# => 'CAT_NAME'

<<SQL
select #{Category.properties[:id].field}
from #{Category.storage_name} as C
where C.#{Category.properties[:id].field} = categories.#{Category.id}
  and #{Category.properties[:name].field} = ?
SQL
# select id
# from categories as C
# where C.id = categories.id
#   and CAT_NAME = ?

レガシーは辛い。