anyorm

Object Relational Mapper for node.js

架构

AnyORM是一个以DataMapper模式实现的ORM库,整个库由四部分构成:Data、Mapper、Type、Service。

Data主要承载业务逻辑封装,关注数据在具体业务逻辑中的使用。

和ActiveRecord模式的ORM不同之处在于,Data不关心存储服务细节,这些细节都委托给Mapper处理。

这种把业务逻辑和存储服务隔离开的一个好处就是,当业务发展过程中需要迁移到另外一种存储服务之后,业务逻辑层不需要经受太多的改动。

无论数据实际是存储在PostgreSQL这样的关系式数据库,还是在MongoDB这样的NoSQL数据库,最终这些数据都具有大致相同的API。

把数据从PostgreSQL迁移到MongoDB这种情况,完全不改动已有代码基本上是不可能的,我们只能追求如何改得更少,改起来更方便。

Data和Service的中间层,负责从存储服务中读取数据并打包为Data实例,也负责把Data内的数据存储到存储服务。

AnyORM已经封装了常用的关系式数据库Mapper,可以直接使用,也可以根据需要封装自己的Mapper class。

数据类型封装,负责处理数据从存储服务到程序执行空间内的转换,数据有效性等等细节处理。

例如create_time在程序内以javascript Date数据类型的形式存在,但是保存到数据库内需要转换为ISO8601的格式(比如 2014-01-01T00:00:00Z),这种转换由预定义的数据类型方法透明的处理,在Data使用时不需要再费事的自行处理。

AnyORM除了提供一系列常用的数据类型之外,还支持自定义数据类型,满足不同业务的实际需求。

存储服务封装,AnyORM已经提供了常用的关系式数据库(MySQL、PostgreSQL、SQLite3)和Redis服务。

另外Service模块还提供了一个小巧灵活的Service管理模块,实现多个数据源的管理工作。

快速开始

我将以一个简单的留言板应用进行展示,此应用包含两个Model,用户(User)和帖子(Topic),所有的数据存储在PostgreSQL数据库中,数据库服务器的地址是127.0.0.1,数据库的名字是"borad"。

var anyorm = require('anyorm');
var Service = anyorm.Service;
 
// 一旦定义好名字为"db"的服务之后,以后就可以用 Service.get('db') 来使用这个服务。 
anyorm.defineService('db', {
    // 数据库服务实例化方法 
    factoryfunction(options) {
        return new Service.DB.Adapter(options.dsn, options.pool);
    },
 
    // 数据库配置 
    dsn: 'postgres://user:password@127.0.0.1/board',
 
    // 连接池配置 
    pool: {
        max: 8,
        min: 1
    }
});
var User = anyorm.defineData({
    mapper: anyorm.DBMapper,
    service: 'db',
    collection: 'board.users',
    attributes: {
        // 整数类型的自增长主键 
        user_id: {
            type: 'integer',
            primary_key: true,
            auto_generate: true
        },
        // 用户名 
        email: {
            type: 'string',
            pattern: /^[a-z\.\-]+@[a-z\.\-]+\.[a-z]{2,3}$/i,
            strict: true,
            normalizefunction(email) {
                return email.toLowerCase();
            }
        },
        // 密码 
        password: {
            type: 'string',
            strict: true,
            normalizefunction(password) {
                return this._normalizePassword(password);
            }
        },
        // 账号被锁定的时间,默认不锁定 
        lock_time: {
            type: 'datetime',
            allow_null: true,
            strict: true
        },
        // 注册时间,保存后不允许更新 
        register_time: {
            type: 'datetime',
            refuse_update: true,
            default: 'now'
        }
    }
});
 
User.prototype.checkPassword = function(password) {
    return this._normalizePassword(password) === this.password;
};
 
User.prototype.isLocked = function() {
    return !!this.lock_time;
};
 
User.prototype.lock = function() {
    this.lock = new Date;
    return this.save();
};
 
User.prototype.unlock = function() {
    this.lock = null;
    return this.save();
};
 
// 以create_time为slat,把密码转换为md5 
User.prototype._normalizePassword = function(password) {
    // unix timestamp 
    var ts = (this.register_time.getTime() / 1000) >> 0;
    password = password + ts;
 
    var crypt = require('crypto');
    var hash = crypt.createHash('md5');
 
    return hash.update(password).digest('hex');
};
 
// return promise 
User.register = function(emailpassword) {
    return User.findByEmail(email).then(function(user) {
        if (user) {
            throw new Error('Email已经被注册');
        }
 
        var user = new User;
        user.email = email;
        user.password = password;
 
        return user.save();
    });
};
 
// return promise 
User.findByEmail = function(email) {
    email = email.trim();
    assert.ok(email !== '', 'Email不允许为空');
 
    return User.getMapper().select().where('email = ?', email.toLowerCase()).getOne();
};
 
// return promise 
User.login = function(emailpassword) {
    return User.findByEmail(email).then(function(user) {
        if (!user || !user.checkPassword(password)) {
            return false;
        }
 
        return user;
    });
};
 
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 
 
var Topic = anyorm.defineData({
    mapper: anyorm.DBMapper,
    service: 'db',
    collection: 'board.topics',
    attributes: {
        topic_id: {type: 'integer', primary_key: true, auto_generate: true},
        author: 'integer',
        reply_topic: {type: 'integer', allow_null: true},
        subject: 'string',
        content: 'string',
        create_time: {type: 'datetime', refuse_update: true, default: 'now'}
    }
});
 
// return promise 
Topic.prototype.getAuthor = function() {
    return User.find(this.author);
};
 
// return Service.DB.Select 
Topic.prototype.selectReply = function() {
    return Topic.getMapper().select().where('reply_topic = ?', this.getId());
};
 
// return promise 
Topic.post = function(usertopicreply_topic_id) {
    var topic = new Topic;
 
    topic.author = user.getId();
    topic.subject = topic.subject;
    topic.content = topic.content;
 
    if (reply_topic_id) {
        topic.reply_topic = reply_topic_id;
    }
 
    return topic.save();
};

上面的User class已经使用了现成的DBMapper

为了展示如何使用自定义Mapper,这里将利用另外一个现成的DBCacheMapper来为User数据增加一层缓存

当User.find(id)时,会先在缓存中查询,一旦命中就不需要再到数据库内查询。当没有命中缓存时,把数据库的查询结果缓存起来,下次直接使用。当修改了User数据时,删除已经缓存的老数据。

缓存机制对于User是透明的,不会改变User的使用方式。

var Promise = require('bluebird');
 
// 定义缓存使用的Redis服务 
anyorm.defineService('redis', {
    factoryfunction(options) {
        return Service.Redis.createPool(options.pool, options.client);
    },
 
    // redis配置 
    client: {
        host: '127.0.0.1',
        port: 6379,
    },
 
    // redis连接池配置 
    pool: {
        max: 8,
        min: 3,
    }
});
 
// UserMapper继承自anyorm.CacheDBMapper 
// CacheDBMapper继承自DBMapper 
// CacheDBMapper需要自行实现缓存读写细节 
var UserMapper = anyorm.defineMapper({
    _getCacheKeyfunction(id) {
        return 'user:'+id;
    },
    _getCachefunction(id) {
        var redis = Service.get('redis');
        var key = this._getCacheKey(id);
 
        return redis.execute('hGetAll', key);
    },
    _deleteCachefunction(id) {
        var redis = Service.get('redis');
        var key = this._getCacheKey(id);
 
        return redis.execute('del', key);
    },
    _saveCachefunction(idrecord) {
        var redis = Service.get('redis');
        var key = this._getCacheKey(id);
        var ttl = this.getOption('cache_ttl');
 
        return redis.acquire().then(function(client) {
            return new Promise(function(resolvereject) {
                client.multi()
                      .hmset(key, record)
                      .expire(key, ttl)
                      .exec(function(errorresult) {
                          redis.release(client);
 
                          error ? reject(error) : resolve(reject);
                      });
            });
        });
    }
}, anyorm.CacheDBMapper);
 
var User = defineData({
    mapper: UserMapper,
    // 缓存有效时间,单位:秒 
    cache_ttl: 300,
    // 其它配置 
    // ... 
});
var assert = require('assert');
 
// 定义新的数据类型 
anyorm.defineType('email', {
    normalizefunction(value) {
        if (value === '' || value === null) {
            return null;
        }
 
        assert.equal(typeof value, 'string', 'email不是字符串');
 
        value = value.trim();
        if (value === '') {
            return null;
        }
 
        assert.ok(/^[a-z\.\-]+@[a-z\.\-]+\.[a-z]{2,3}$/i.test(value), '非法的email');
 
        return value.toLowerCase();
    }
});
 
// 现在可以声明email字段的数据类型是"email" 
var User = anyorm.defineData({
    // 其它配置 
    // ... 
    attributes: {
        email: {type: 'email'},
        // 其它字段 
        // ... 
    }
});

注册新用户

User.register('Foo@example.com', 'my password')
    .then(function(user) {
        console.log('User register success!')
    })
    .catch(function(error) {
        console.log('User register failed');
        console.log(error);
    });

登录验证

User.login('foo@example.com', 'my password').then(function(user) {
    if (user) {
        console.log('Login success');
    } else {
        console.log('Login failed');
    }
}).catch(function(error) { console.log(error); });

修改密码

User.findByEmail('foo@example.com')
    .then(function(user) {
        // password赋值后会自动转换为md5值 
        user.password = 'new password';
 
        return user.save();
    })
    .catch(function(error) {
        console.log(error);
    });

留言

var post = {
    subject: 'hello world!';
    content: 'bla bla bla ...';
};
 
Topic.post(user, post).save().then(function(topic) {
    console.log('Topic post success');
    console.log(topic);
 
    return topic.getAuthor();
}).then(function(author) {
    console.log('Author is', author.email);
}).catch(function(error) { console.log(error); });

查询留言

user.selectTopic().then(function(topics) {
    for (var i = 0; topic = topics[i++];) {
        console.log(topic);
    }
}).catch(function(error) { console.log(error); });
 
// 如果查询结果很大,一次性载入会很耗费内存,可以用stream的方式来处理 
User.selectTopic({return_stream: true}).then(function(stream) {
    return new Promise(function(resolvereject) {
        stream.on('data', function() {
            var topic = stream.ready();
            console.log(topic);
        });
 
        stream.on('error', reject);
 
        stream.on('end', resolve);
    });
}).catch(function(error) { console.log(error); });