0.2.13 • Published 10 years ago

anyorm v0.2.13

Weekly downloads
2
License
MIT
Repository
github
Last release
10 years ago

架构

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

Data(数据)

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

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

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

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

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

Mapper(业务数据和存储数据之间的映射)

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

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

Type(数据类型)

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

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

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

Service(存储服务读写)

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

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

快速开始

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

定义Service

var anyorm = require('anyorm');
var Service = anyorm.Service;

// 一旦定义好名字为"db"的服务之后,以后就可以用 Service.get('db') 来使用这个服务。
anyorm.defineService('db', {
    // 数据库服务实例化方法
    factory: function(options) {
        return new Service.DB.Adapter(options.dsn, options.pool);
    },

    // 数据库配置
    dsn: 'postgres://user:password@127.0.0.1/board',

    // 连接池配置
    pool: {
        max: 8,
        min: 1
    }
});

定义Data

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,
            normalize: function(email) {
                return email.toLowerCase();
            }
        },
        // 密码
        password: {
            type: 'string',
            strict: true,
            normalize: function(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(email, password) {
    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(email, password) {
    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(user, topic, reply_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();
};

定义Mapper

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

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

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

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

var Promise = require('bluebird');

// 定义缓存使用的Redis服务
anyorm.defineService('redis', {
    factory: function(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({
    _getCacheKey: function(id) {
        return 'user:'+id;
    },
    _getCache: function(id) {
        var redis = Service.get('redis');
        var key = this._getCacheKey(id);

        return redis.execute('hGetAll', key);
    },
    _deleteCache: function(id) {
        var redis = Service.get('redis');
        var key = this._getCacheKey(id);

        return redis.execute('del', key);
    },
    _saveCache: function(id, record) {
        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(resolve, reject) {
                client.multi()
                      .hmset(key, record)
                      .expire(key, ttl)
                      .exec(function(error, result) {
                          redis.release(client);

                          error ? reject(error) : resolve(reject);
                      });
            });
        });
    }
}, anyorm.CacheDBMapper);

var User = defineData({
    mapper: UserMapper,
    // 缓存有效时间,单位:秒
    cache_ttl: 300,
    // 其它配置
    // ...
});

自定义Type

var assert = require('assert');

// 定义新的数据类型
anyorm.defineType('email', {
    normalize: function(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(resolve, reject) {
        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); });
0.2.13

10 years ago

0.2.12

10 years ago

0.2.11

10 years ago

0.2.10

10 years ago

0.2.9

10 years ago

0.2.8

10 years ago

0.2.7

10 years ago

0.2.6

10 years ago

0.2.5

10 years ago

0.2.4

10 years ago

0.2.3

10 years ago

0.2.2

10 years ago

0.2.1

10 years ago

0.2.0

10 years ago

0.1.0

10 years ago