sails.jsのアソシエーション周りまとめ

最近Sails.jsでプロジェクトを一つ回してみたのでそのまとめ。アソシエーション編。
Sails.js、というかそこで使われているO/R Mapperのwaterlineでアソシエーションをどう記述するか。

とりあえず、findだけまとめる。createとかは後でまとめます。

# アソシエーション別
## One to Many
「記事に複数の画像が紐付いている」という場合。`aritcles`と`article_images`という2つのテーブルがあり、`article_images`の`article_id`が`article`を参照しているというケース。

### モデル定義

```js:Article.js
module.exports = {
  tableName: 'articles',

  attributes: {
    title: {
      type: 'string',
      required: true
    },
    contents: {
      type: 'string',
      required: true
    },

    // collection: JOIN先のモデル名(テーブル名ではない)
    // via: JOIN先モデルで定義した要素名
    images: {
      collection: 'ArticleImage',
      via: 'article'
    }
  }
}
```

``` js:ArticleImage.js
moduel.exports = {
  tableName: 'article_images',

  attributes: {
    path: {
      type: 'string',
      required: true
    },

    // columnName: 実際のカラム名(同じ場合は省略可能)
    // model: 外部キーが指す`module.exportで定義した`モデル名(テーブル名では無い)
    article: {
      columnName: 'article_id',
      model: 'Article',
      type: 'integer',
      required: true
    }
  }
}
```

### query
Articleに紐付いた全ての画像をJOINして取得する

```js
// populateでmodelに定義した要素名を指定
Article.findOne(1).populate('images').then(function(article) {
  // success
}).catch(function(error) {
  // error
});
```

## Many to Many
交差テーブルを利用したMany to Manyの定義。「記事に複数のタグがひも付き、同じタグが紐付いた記事が複数ある」場合。

### モデル定義

```js:Article.js
module.exports = {
  tableName: 'articles',

  attributes: {
    title: {
      type: 'string',
      required: true
    },
    contents: {
      type: 'string',
      required: true
    },

    // collection: JOIN先のモデル
    tags: {
      collection: 'Tag'
    }
  }
}
```

```js:Tag.js
moduel.exports = {
  tableName: 'tags',

  attributes: {
    name: {
      type: 'string',
      required: true,
      unique: true
    },

    // JOIN先のモデル
    articles: {
      collection: 'Article'
    }
  }
}
```

重要なのは交差テーブル

```js:ArticleTag.js
module.exports = {
  tableName: 'articles_tags',
  // ここはモデル名ではなくテーブル名
  tables: ['articles', 'tags'],
  junctionTable: true,

  attributes: {
    article: {
      columnName: 'article',
      type: 'integer',
      foreignKey: true,
      references: 'tag',
      on: 'id',
      onKey: 'id',
      via: 'article'
    },
    tag: {
      columnName: 'article',
      type: 'integer',
      foreignKey: true,
      references: 'tag',
      on: 'id',
      onKey: 'id',
      via: 'article'
    }
  }
}
```

### query
#### find
上記のモデル定義ならArticle/TagのどちらからでもJOIN出来る

```js
// 紐付いたTagと共にArticleを取得
Article.findOne(1).popualte('tags').then(function(article) {
  // success
}).catch(function(error) {
  // error
});

// 紐付いたArticleとともにTagを取得
Tag.findOne(1).popualte('articles').then(function(tag) {
  // success
}).catch(function(error) {
  // error
});
```

#### create
junctionTableにはアクセス出来ないので、createはO/R Mapperに任せる。

```
Tag.create({name: 'hoge'}).then(function(tag) {
  // created {id: 1, name: 'hoge'}
}).catch(function(error){ 
});

Article.create({title: 'test', tags: [1]}).then(function(article) {
  // ArticleのモデルとjunctionTableが生成される
}).catch(function(error) {

});
```

### 注意点
#### 交差テーブルにはアクセス出来ない
次のようなコードはエラーになる。

```js
ArticleTag.find({article: 1}).then(function(article) {

}).catch(function(error) {
  // ERROR!!!!!!!
});
```

これはSails.jsがモデル定義を読みこみ、グローバルオブジェクトとして格納する際に`junctiontable = true`のモデルは破棄(グローバルからアクセス出来ないように)しているため。

#### 交差テーブルの要素名とカラム名は合わせる
何故か、交差テーブルの要素名とカラム名は合わせないといけない。例えば次のようなものはqueryの実行時エラーになる。[それっぽいIssue](https://github.com/balderdashy/waterline-schema/issues/20)が立ってるのでそのうち直るはず。

```js:ArticleTag.js
module.exports = {
  tableName: 'articles_tags',
  tables: ['articles', 'tags'],
  junctionTable: true,

  attributes: {
    article: {
      // これがエラー
      columnName: 'article_id',
      type: 'integer',
      foreignKey: true,
      references: 'tag',
      on: 'id',
      onKey: 'id',
      via: 'article'
    },
    tag: {
      columnName: 'article',
      type: 'integer',
      foreignKey: true,
      references: 'tag',
      on: 'id',
      onKey: 'id',
      via: 'article'
    }
  }
}
```


# find()
## いわゆるWHERE
`where()`を使う。`find()`の引数に書いてもいい。

```js
Article.find({name: 'hoge'}).then(function(articles){}).catch(function(error){});

Article.find().where({name: 'hoge'}).then(function(articles){}).catch(function(error){});
```

## いわゆるJOIN

`populate()`を使う。複数連結したい場合は続けて書く。

```js
Article.find().populate('tag').populate('images').then(function(articles){}).catch(function(error){});
```

## その他
ソートとページネーション

```js
Article.find().sort('createdAt DESC').paginate({page: 1, limit: 5}).then(function(articles){}).catch(function(error){});
```

## ハマりポイント
### populateの条件文
こんな感じで書くが、微妙なポイントがあり、~~**populateの第2引数に指定するwhere句は、`カラム名`を指定する(通常のwhereはモデルに定義した要素名)**。謎。~~

追記
waterline送ったプルリクエストが通ったので、カラム名でもモデルのattribute名でも指定できるようになった。waterline v0.10.25から。

```js
Article.find().populate('images', {where: {'width': {'<=' : 480}}}}.then(function(articles){}).catch(function(error){});
```