Meet ORM: JugglingDBsorf of documentation driven development

注意:本セクションで説明される機能は、developブランチのみで使用でき、一部機能は開発中となります。

1. データベースの設定

config/database.jsonファイルにデータベースのアダプタ、接続先、接続方法を記述します。

{ "development":
  { "driver":   "redis"
  , "host":     "localhost"
  , "port":     6379
  }
, "test":
  { "driver":   "memory"
  }
, "staging":
  { "driver":   "mongoose"
  , "url":      "mongodb://localhost/test"
  }
, "production":
  { "driver":   "sequelize"
  , "host":     "localhost"
  , "post":     3306
  , "database": "nodeapp-production"
  , "username": "nodeapp-prod",
  , "password": "t0ps3cr3t"
  }
}

使用可能なアダプタのリストはこちらのテストを確認して下さい。また、schemaメソッドを使用して、スキーマ定義の中で直接データベースを指定することもできます。

schema 'redis', url: process.env.REDISTOGO_URL, ->
    define 'User'
    # other definitions for redis schema

schema 'mongoose', url: process.env.MONGOHQ_URL, ->
    define 'Post'
    # other definitions for mongoose schema

このスキーマの全てが同時に動作し、User.hasMany(Post)のような異なるスキーマ間の関係も記述することさえできます。クールだとおもいませんか?これがJugglingDBと呼ぶ理由です。

2. スキーマの定義

データベースのエンティティを記述するメソッドを定義し、propertyメソッドにてフィールドのタイプを指定してください。このメソッドは以下の引数を持ちます。

  • プロパティ名
  • タイプ: Date, Number, Boolean, Text, String (Stringの場合は省略できます)
  • オプション: Object {default: ‘default value’, index: true}

javascript: db/schema.js

var Person = define('Person', function () {
    property('email', {index: true});
    property('active', Boolean, {default: true});
    property('createdAt', Date);
});

var Book = define('Book', function () {
    property('title');
    property('ISBN');
});

coffeescript: db/schema.coffee

Person = define 'Person', ->
    property 'email', index: true
    property 'active', Boolean, default: true
    property 'createdAt', Date, default: Date
    property 'bio', Text
    property 'name'

Book = define 'Book', ->
    property 'title'
    property 'ISBN'

3. リレーションの記述

現在サポートされている関係:hasMany, belongTo

もちろんオブジェクト間の関係も設定し、拡張することができます。

User.hasMany(Post,   {as: 'posts',  foreignKey: 'userId'});
// creates instance methods:
// user.posts(conds)
// user.posts.build(data) // like new Post({userId: user.id});
// user.posts.create(data) // build and save
// user.posts.find

Post.belongsTo(User, {as: 'author', foreignKey: 'userId'});
// creates instance methods:
// post.author(callback) -- getter when called with function
// post.author() -- sync getter when called without params
// post.author(user) -- setter when called with object

また、多くのアソシエーションをもっている場合には、scopeを使うこともできます。例として、Postのスコープは、

Post.scope('published', {published: true})

これは単に、全てのメソッドのショートカット呼び出しを作成するために使われます。

Post.published(cb) // same as Post.all({published: true});

アソシエーションと同時に使うこともできます。

user.posts.published(cb); // same as Post.all({published: true, userId: user.id});

4. バリデーションの設定

現在サポートされているバリデーションは以下の通りです。

  • presence
  • length
  • numericality
  • inclusion
  • exclusion
  • format

バリデーションはsaveメソッドを使用する場合にバリデーションがスキップできる場合にもcreate, save, updateAttributesの後に実行されます。

obj.save({validate: false});

バリデーションはisValidメソッドをつかって明示的に呼び出すこともできます。

通常、全てのバリデーションはエラーの場合にはエラーメッセージの配列ハッシュをオブジェクトのメンバーであるerrorsとして結果返します。

obj.errors 
{
    email: [
        'can\'t be blank',
        'format is invalid'
    ],
    password: [ 'too short' ]
}

saveメソッドのパラメータにthrows: trueを渡すと、例外を発生させることができます。

// be carefull, now it will throw Error object
obj.save({throws: true});

バリデーションを設定する為には、クラス内で、コンフィギュレータをコールします。

Person.validatesPresenceOf('email', 'name')
Person.validatesLengthOf('password', {min: 5})

各コンフィギュレータは実際にバリデータが動作する方法をオプションの最後の引数として文字列を設定します。もちろんそれは、各バリデータによりますが、以下のような一般的なオプションが設定可能です。

  • if
  • unless
  • message
  • allowNull
  • allowBlank

ifunlessメソッドは条件に応じたバリデーションのスキップを実現します。これは関数または文字列で指定することができます。この関数はバリデーションが実行されるオブジェクトのコンテキスト内でコールされ、実行します。文字列が渡された場合には上述のオプションから該当するものを実行します。

messageでは文字列または(バリデータに依存した)オブジェクトによってエラーメッセージを設定することができます。下記例を参照してください。

allowNullallowBlankの説明は必要ないでしょう:)

他のタイプのバリデーションの例を以下に示します。

LENGTH
User.validatesLengthOf 'password', min: 3, max: 10, allowNull: true
User.validatesLengthOf 'state', is: 2, allowBlank: true
user = new User validAttributes

user.password = 'qw'
test.ok not user.isValid(), 'Invalid: too short'
test.equal user.errors.password[0], 'too short'

user.password = '12345678901'
test.ok not user.isValid(), 'Invalid: too long'
test.equal user.errors.password[0], 'too long'

user.password = 'hello'
test.ok user.isValid(), 'Valid with value'
test.ok not user.errors

user.password = null
test.ok user.isValid(), 'Valid without value'
test.ok not user.errors

user.state = 'Texas'
test.ok not user.isValid(), 'Invalid state'
test.equal user.errors.state[0], 'length is wrong'

user.state = 'TX'
test.ok user.isValid(), 'Valid with value of state'
test.ok not user.errors
NUMERICALITY
User.validatesNumericalityOf 'age', int: true
user = new User validAttributes

user.age = '26'
test.ok not user.isValid(), 'User is not valid: not a number'
test.equal user.errors.age[0], 'is not a number'

user.age = 26.1
test.ok not user.isValid(), 'User is not valid: not integer'
test.equal user.errors.age[0], 'is not an integer'

user.age = 26
test.ok user.isValid(), 'User valid: integer age'
test.ok not user.errors
INCLUSION
User.validatesInclusionOf 'gender', in: ['male', 'female']
user = new User validAttributes

user.gender = 'any'
test.ok not user.isValid()
test.equal user.errors.gender[0], 'is not included in the list'

user.gender = 'female'
test.ok user.isValid()

user.gender = 'male'
test.ok user.isValid()

user.gender = 'man'
test.ok not user.isValid()
test.equal user.errors.gender[0], 'is not included in the list'
EXCLUTION
User.validatesExclusionOf 'domain', in: ['www', 'admin']
user = new User validAttributes

user.domain = 'www'
test.ok not user.isValid()
test.equal user.errors.domain[0], 'is reserved'

user.domain = 'my'
test.ok user.isValid()
FORMAT
User.validatesFormatOf 'email', with: /^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i
user = new User validAttributes

user.email = 'invalid email'
test.ok not user.isValid()

user.email = 'valid@email.tld'
test.ok user.isValid()