TypeScriptでmongoose と戦う日記 #3 僕が考えたファイル構成

# はじめに
ここ最近ずっとmongooseと格闘していましたが、とりあえずやりたい事はやれるようになったため、簡単なコードと一緒に書き留めておこうと思います。

# 前提条件
nameとageを持つUserというドキュメントを扱う。
ただし、Userはそれら二つとは別に、DBには保存しない値としてcacheを持つ必要がある。
また、Userモデルは、mongooseのプラグインによって拡張され、'findOrCreate'メソッドを新たに持つ。

## ディレクトリ構成
declare.d.tsなどは省略しております。

+ app.ts
+ model/
    + User.ts
    + mongoose/
      + document/
        + BaseDocument.ts
        + UserDocument.ts
    + model/
        + UserModel.ts
    + schema/
        + BaseSchema.ts
        + UserSchema.ts

こんな感じになりました。
model直下にUserというモデルが存在し、それをapp.tsが利用する感じです。
ただし、一つのUserモデルを作るために、mongooseのクラスやインターフェースを継承した

+ Schema
+ Model
+ Document

の三つが必要になっています。これらについてまとめていきます。

## Schema
その名の通り、DBになにを格納するかです。前提条件の通り、Userはnameとageを持つため、以下のような内容となっています。

```ts:UserSchema.ts
import BaseSchema = require('./BaseSchema');

class UserSchema extends BaseSchema {
  constructor() {
    var schema = {
      name : String,
      age : Number
    };
    var options = {};
    super(schema, options);
    this.plugin(require('mongoose-findorcreate'));
  }

}

export = UserSchema;
```

mongooseのスキーマ定義用オブジェクトを内部で定義し、親クラスのコンストラクタに渡します。外部から親クラスの初期化に必要な値を受け取る事は出来ないため、誰がどう使っても同じ物が返されます。
ちなみに継承元は以下のような感じです。

```ts:BaseSchema.ts
import mongoose = require('mongoose');

class BaseSchema extends mongoose.Schema {
  constructor(schema: any = {}, options: any = {}) {
    if (!options.noTimestamp) {
      schema.created = {type : Date , default:Date.now};
      schema.updated = {type : Date , default:Date.now};
    }
    super(schema, options);
  }

}

export = BaseSchema;
```

BaseSchemaはmongooseのSchemaを継承しているため、そのままmongooseに渡す事が出来ます。
createdとupdatedは、だいたいのドキュメントに必要かな、と思い、BaseSchemaで定義しました。いらない場合は、継承先でoptionsにnoTimestampを挿す、という方針です。

## Model
前述のSchemaの成果物を定義します。**ここが少し納得できなかった部分**です。

```ts:UserModel.ts
import mongoose = require('mongoose');
import UserDocument = require('../document/UserDocument');

interface UserModel extends mongoose.Model<UserDocument> {
  findOrCreate(opt:{}, callback:Function):void;
}
```

UserDocumentを扱う専用のModelとして、ジェネリック型であるmongoose.Modelを継承しました。
普通にやるだけならcastだけでなんとかなるはずですが、今回はプラグインで拡張されたメソッドが新たに生えるため、mongoose.Modelのinterfaceを拡張してやる必要がありました。
が、外部モジュールのinterfaceは後から拡張できないらしく、さらに[ジェネリッククラスの継承がうまくいかなかった](http://qiita.com/orzngo/items/18733885162edc6807e1)ため、前述のようにジェネリック型のModelに型を差し込み、それを継承する形になりました。

そのため、Schemaでやったような、mongooseのモデルを継承したBaseModelを作り共通処理はそこに書き、それを継承していろいろな物を作っていく、という事が出来なくなっています。
例えば、findOrCreateメソッドをほかのモデルにも生やす場合、全てのモデルに前述のようにメソッドを書く必要があります。とても悲しい。たすけて。

## Document
Modelによって生まれる、mongooseのデータの実態がここで定義されます。mongooseが返すDocumentオブジェクトには、僕が定義したUserのnameもageも生えていないので、ここでその存在をアピールしなければなりません。

```ts:UserDocument.ts
import BaseDocument = require('./BaseDocument');

interface UserDocument extends BaseDocument {
  name : String;
  age : Number;
}

export = UserDocument;
```

特に何もありません。nameとageの存在を主張しているだけです。
ちなみにBaseDOcument.tsの中身ですが、

```ts:BaseDocument.ts
import mongoose = require('mongoose');

interface BaseDocument extends mongoose.Document {
  created : Date;
  updated : Date;
}

export = BaseDocument;
```

これも特別な事はしていません。BaseSchemaで定義しているcreatedとupdatedの存在を主張しているだけです。
基本的にSchemaとDocumentは対になって存在しています。
ただ、createdとupdatedが存在しないSchemaを作成可能で、それにこのDocumentが対応していない事に、今書いていて気づきました。
mongoose.Documentを継承するクラスを二つ作り、日付あるよDocumentと日付ないよDocumentを作った方がよかったかもしれません。Schemaも同様。

## ３要素の成果物としてのUser
いわゆる、mongooseを操作する、ぼくの中のモデルとしてのUser、です。ちょっと日本語難しい。
DBに書き込まない = Documentのプロパティとして定義できない物は、このモデルが持ちます。
基本的には、UserDocumentとUserModelのラッパーとして存在し、外からの命令をそれらに橋渡しします。

```ts:User.ts
import mongoose = require('mongoose');
import UserSchema = require('./mongoose/schema/UserSchema');
import UserModel = require('./mongoose/model/User');
import UserDocument = require('./mongoose/document/UserDocument');

class User {
  static schema:UserSchema = new UserSchema();

  static UserModel:UserModel = <UserModel>mongoose.model('User', User.schema);

  //モデルのインスタンス
  private _user:UserDocument;

  //その他
  private _cache:String

  static findOrCreate(opt:{}, callback:(err:any, instance:User) => void):void {
    User.UserModel.findOrCreate(opt, (err:any, document:UserDocument) => {
      var user:User = new User(document);
      callback(err, user);
    });
  }

  constructor (document:UserDocument);
  constructor (name:String);
  constructor (value:any, age?:Number, cache?:String) {
    if (value instanceof String) {
      this._user = new User.UserModel({name:value, age:age});
    } else {
      this._user = value;
    }
    this._cache = cache;
  }

  /**
   * なんかこの辺にdocumentのプロパティに対してgetterとか生える
   * もちろん、this._cacheのgetterも生える
   */

  save(callback:(error:any, res:UserDocument) => void):void {
    this._user.save(callback);
  }
```

コンストラクタをオーバーロードしています。UserDocumentのインスタンスが渡された場合、nameとageが渡された場合それぞれで、Userモデルをnewできるようになっています。

ここまでつくってようやく、利用側でこのUserModelを読み込めば利用できるようになります。

# まとめ
mongooseを使わなかったらもっと簡単になったんじゃないかとだんだん思い始めていますが、とりあえずやりたい事は出来てしまったためよしとします。
きっと、もっと複雑になった時、mongooseを使ってよかったって思えるんだと、そういう風に自分を納得させています。
ジェネリッククラスの継承の仕方が分かればもう少し手間が減るかなと感じているので、もう少し調べてみようと思います。