@kyanny's blog

My thoughts, my life. Views/opinions are my own.

MongoMapper で one association を持つモデルを STI するときは one association を :foreign_key オプション付きで再定義する

以下のようなモデルを定義して、 User を継承した WrapUser のインスタンスが one アソシエーションのメソッド呼び出しをすると関連モデルではなく nil が返る、というので少しハマった。

class User
  include MongoMapper::Document
  one :membership
end

class Membership
  include MongoMapper::Document
  belongs_to :user
end

class WrapUser < User
end

悪い実装のサンプルコードと実行結果: https://gist.github.com/kyanny/cb3cfb759dd0fc8c0ec8e0ad175bfd47

MongoMapper のソースのあちこちに byebug を仕込んだりしたが、 MongoDB のクエリのログを見れば一目瞭然で、 Single Table Inheritance するとインスタンスのクラス名が変わるので、 MongoMapper が内部で MongoDB の find クエリを組み立てる際の条件に使われるキー名も変わってしまう。

testing['memberships'].find({:wrap_user_id=>BSON::ObjectId('583c72606200b03f8b000001')}).limit(-1)

今回の例でいうと、 wrap_user_id というキーはどこにも定義されておらず、コレクション内のドキュメントもこのキーを持っていないので、このクエリにマッチするドキュメントは無く、結果として nil が返る。

これに対処するには、 one アソシエーションの定義時に :foreign_key オプションを付けて、誤った外部キー名が自動生成されないようにすればよい。

class WrapUser < User
  one :membership, foreign_key: :user_id
end

正しい実装のサンプルコードと実行結果: https://gist.github.com/kyanny/42d961b252a5f1aca797f8482d959ca5

クエリはこうなる testing['memberships'].find({:user_id=>BSON::ObjectId('583c72996200b0416a000001')}).limit(-1)

差分はこれだけ。 gist.github.com


特定の条件を満たしたときだけ MongoMapper なモデルに MixIn されるモジュールがあって、それに対するユニットテストの実装に問題があった。テストスイートの実行中、ターゲットのモデルは MixIn されていない綺麗な状態を保ちたいので、ターゲットのモデルを継承したモデルをテストスクリプトの中で定義し、そのモデルのインスタンスに対して MixIn が提供するインスタンスメソッドのユニットテストを書いていた。その中で one アソシエーションの関連先モデルを呼び出すコードを実行したら、 nil で落ちてしまった、というのがことの発端だった。上に書いたのと同じ処置を施したら nil で落ちなくなった。