もともとブログの投稿はRails経由で更新する予定でしたが、最近はWebに対する熱が急激に冷めてきたというかやっぱり使い慣れた方法というか、エディタは使い慣れたほうがよいかなということでまた振り出しに戻ってしまいました。 近況としてはリモートワークでMERNスタックのユーザ認証システムを作っています。 そこでの要件は個人ユーザと企業用のユーザが独立していて、ログインはメールアドレスで判別して個人なら個人のページへ、企業ユーザは企業用のページへ振り分けたい。 何も考えずに実装しようとしたら、それぞれIndividualUserCompanyUserみたいなモデルが分かれていていずれのテーブルにメールアドレスが重複していなければサインアップ可能みたいな方法を考えていました。

さすがにその方法はないだろうということで、RailsではSTI(単一テーブル継承)というテクニックがありました。 ちょうどクラスの継承に似ていて、IndividualUserCompanyUserに対して共通のUserモデルに認証の関数を用意すれば関心の分離にもなるし、KISSの原則も守れます。 MongoDBないし、MongooseでSTIを実装しようと思ったらどうすればよいのでしょうか。

そこで出てくるのがmodel.discriminator()という関数です。 Railsではusersテーブルに対してtypeというカラムにクラス名を入れましたが、Mongooseでは{discriminatorKey: 'kind'}と指定するとkindにモデルのクラス名を自動的に格納してくれるようです。 さっそくコード例をみてみましょう:

var options = { discriminatorKey: 'kind' };

// 親のクラス: User
var userSchema = new mongoose.Schema({ email: String }, options);
var User = mongoose.model('User', userSchema);

// 子のクラス1: IndividualUser
var individualSchema = new mongoose.Schema({ name: String }, options);
var IndividualUser = User.discriminator('IndividualUser', individualSchema);

// 子のクラス2: CompanyUser
var companySchema = new mongoose.Schema({ role: String }, options);
var CompanyUser = User.discriminator('CompanyUser', companySchema);

こうすることでIndividualUserroleが参照できず、CompanyUsernameに参照できませんが、emailにはどちらも参照可能です。 興味深いことにmodel.discriminator()で作られたモデルはそれぞれ呼び出し元で意味が変わります。

NOTE: 備忘録なので動作未確認です。

Promise.all([
  IndividualUser.create({ email: 'foo@example.com' }),
  CompanyUser.create({ email: 'foo@example.com' }),
]).then(() => {
  // 個人ユーザのみ
  IndividualUser.count({ email: 'foo@example.com' }).then((count) =>
    assert.equal(count, 1)
  );

  // ユーザ全体
  User.count({ email: 'foo@example.com' }).then((count) =>
    assert.equal(count, 2)
  );
});

このようにそれぞれのモデルで意図した検索の仕方ができるので、ログイン時はUserで、ログイン後はIndividualUserでといったような検索が可能です。

MongooseやMongoDBは諸事情により敬遠していたのですが、いざ使い始めるとNoSQLは便利だなぁと思いました。