精华 一个自用的极简 ORM,带三方缓存支持——Toshihiko
发布于 4 年前 作者 XadillaX 8791 次浏览 最后一次编辑是 3 年前 来自 分享

目前花瓣网和大搜车都有项目在用该 ORM。

使用很简单,且在上层没做类似于 group byjoin 等降低效率的 API 支持,因为 mysql 本身效率就不是特别高。

支持三方缓存——比如 memcached,也可以自己实现一个 toshihiko-xxx 作为自己的缓存层。

Repo 地址在:https://github.com/XadillaX/Toshihiko

具体用法看文档。


Toshihiko

Toshihiko Toshihiko Build Status Coverage Status Code Quality License

Join the chat at https://gitter.im/XadillaX/Toshihiko

A simple ORM for node.js in Huaban with :heart:. For performance, this ORM does not provide operations like in, group by, join and so on.

Toshihiko Toshihiko

Installation

$ npm install toshihiko

Document

Initialize

You should create a Toshihiko object to connect to MySQL:

var T = require("toshihiko");
var toshihiko = new T.Toshihiko(database, username, password, options);

Options can include these things:

  • host: hostname or IP of MySQL. Defaults to localhost.
  • port: port of MySQL. Defaults to 3306.
  • cache: if you want to cache support, let it be an cache layer object or cache layer configuration which will be mentioned below. Defaults to undefined.
  • etc… (All options in module mysql will be OK)

Cache

Toshihiko now is using new cache layer! You can choose your cache layer by your self!

Pass an object to cache of options like:

var toshihiko = new T.Toshihiko(database, username, password, {
    cache: YOUR_CACHE_LAYER
});

The YOUR_CACHE_LAYER may be an instance of Toshihiko cache layer object like toshihiko-memcacehd (you can implement a cache layer by yourself).

What’s more, YOUR_CACHE_LAYER may be a configuration object which should include name or path.

For an example,

var toshihiko = new T.Toshihiko(database, username, password, {
    cache: {
        name: "memcached",
        servers: "...",
        options: {}
    }
});

will search for package toshihiko-memcached and pass servers, options to create a toshihiko-memcached object. By default, Toshihiko support memcached as cache layer by using package toshihiko-memcacehd.

You can get the cache object in Toshihiko by getting the variable:

var cache = toshihiko.cache;

Define a Model

Define a model schema:

var Model = toshihiko.define(tableName, [
    { name: "key1", column: "key_one", primaryKey: true, type: Toshihiko.Type.Integer },
    { name: "key2", type: Toshihiko.Type.String, defaultValue: "Ha~" },
    { name: "key3", type: Toshihiko.Type.Json, defaultValue: [] },
    { name: "key4", validators: [
        function(v) {
            if(v > 100) return "`key4` can't be greater than 100";
        },
        function(v) {
            // blahblah...
        }
    ] },
    { name: "key5", type: Toshihiko.Type.String, allowNull: true }
], options);

You can add extra model functions by yourself:

Model.sayHello = function() {
    this.find(function(err, rows) {
        console.log(err);
        console.log(rows);
    });
};

options is optional. You can specify Memcached here if you haven’t defined it in Toshihiko. Otherwise, you can let it be null when you don’t want to use Memcached in this Model but you had specify it in Toshihiko.

Query & Update

Toshihiko uses chain operations. Eg:

Model.where(condition).limit(limit).orderBy(order).find(callback);
Model.where(condition).limit(limit).delete(callback);
Model.findById(primaryKeysId, callback);
Model.where(condition).update(data, callback);

where

condition is an JSON object with keys:

  • A field name
  • $and
  • $or
Field Name
Value

For field name, the value can be a certain value. Eg:

{
    key1: 1
}
Operators

The value can be a JSON object with comparison operators $eq / ===, $neq / !==, $gt(e) / >(=), $lt(e) / <(=), $like.

Eg:

{
    keys1: {
        $neq: value
    }
}

value can be a certain value or an array with logic AND.

Eg. $neq: 5 or $neq: [ 5, 2 ].

Logic

You can use logic symbols as well:

{
    keys1: {
        $or: {
            $eq: 1,
            $neq: 2
        }
    }
}

Notice: you can define logic and operators with many many levels.

$and And $or

You can use these two logic with many many levels.

{
    $or: {
        $or: { $or: ... },
    }
}

And the last level can be like that:

{
    $and: {
        KEY: { REFER TO ABOVE `Field Name` }
    }
}

limit

For examples:

foo.limit("1");         ///< skip 1
foo.limit("0,30");      ///< skip 0, limit 30
foo.limit([ 0, 30 ]);   ///< skip 0, limit 30
foo.limit([ 1 ]);       ///< skip 1
foo.limit({ skip: 0, limit: 1 });   ///< skip 0, limit 1
foo.limit({ skip: 1 }); ///< skip 1
foo.limit({ limit: 1 });///< limit 1

orderBy

For examples:

foo.orderBy("key1 asc");
foo.orderBy([ "key1 asc", "key2 desc" ]);
foo.orderBy({ key1: "asc", key2: "desc", key3: 1, key4: -1 });

count

Count the records with a certain condition:

foo.where(condition).count(function(err, count) {});

find

With the conditions, limit and orders to find all records:

foo.where(condition).find(function(err, rows) {
    //...
}, withJson);

Notice: the parameter withJson is an optional parameter. If it’s true, elements in rows are JSON objects. Otherwise, they are all Yukari objects.

findOne

It’s similar with find, but it will just find only one record.

foo.where(condition).findOne(function(err, row) {
    //...
}, withJson);

Notice: withJson is the same as above.

findById

foo.findById(primaryKeysId, function(err, bar) {
}, withJson);

primaryKeysId can be a string or an object.

When there’re several primary keys in one table, this value may be like:

{
    key1: 1,
    key2: 2,
}

If there’s only one primary key, you can just pass a string, number or some other base type value.

For examples:

foo.findById({ key1: 1, key2: 2 }, callback);
foo.findById(1, callback);

update

foo.where(condition).update(data, function(err, result) {});

data is an object that includes your changed data. Eg:

{
    key1: 12,
    key2: "123",
    key3: "{{key3 + 1}}"
}

String with {{...}} will be parsed as SQL statement. For example, you can let it be {{CONCAT(`key3`, ".suffix")}} or any others statement you want to use.

Notice: result is something like:

{ fieldCount: 0,
  affectedRows: 1,
  insertId: 0,
  serverStatus: 2,
  warningCount: 0,
  message: '(Rows matched: 1  Changed: 1  Warnings: 0',
  protocol41: true,
  changedRows: 1 }

delete

foo.where(condition).delete(function(err, result) { /** ... */ });

┏ (゜ω゜)=☞ Promise-Liked

For find, findOne, findById, update and delete, you can use it without callback function.

Whether you used callback function or not, these function will return a ResultPromisor object. You can use it like:

ResultPromisor::success
var Q = foo.find();
Q.success(function(result) { /** ... */ });
ResultPromisor::error
var Q = foo.find();
Q.error(function(err) { /** ... */ });
ResultPromisor::finished
var Q = foo.find();
Q.finished(function(err, result) { /** ... */ });

Yukari Object

Yukari object is the data entity object.

rows in Model.find(function(err, rows) {}) is an array with Yukari objects unless you use withJson parameter.

Also, you can get a new Yukari object by calling Model.build().

We assume all Yukari(s) below are created from Model.find() except Model.build().

Model.build()

You can pass a JSON object to this function to generate a new Yukari object:

Model.build({
    key1    : 1,
    key2    : 2,
    key3    : "3"
});

Yukari::toJSON()

Transform Yukari object to a simple original JSON object:

var json = yukari.toJSON();
console.log(json);

Yukari::insert()

If your Yukari object is created from Model.build(), you should use this function to insert data to database.

var yukari = Model.build({ ... });
yukari.insert(function(err, yukari) {
    //...
});

Yukari::update()

Change this Yukari data to database.

yukari.update(function(err, yukari) {
    //...
});

Notice: "{{..}}" operation is not supported here.

Yukari::save()

If it’s a new Yukari object, it will call insert. Otherwise, it will call update.

yukari.save(function(err, yukari) {
    //...
});

Yukari::delete()

Delete this record from database.

yukari.delete(function(err, affectedRows) {});

Custom Field Type

There’re 4 kind of types in Toshihiko as default.

  • Type.Float
  • Type.Integer
  • Type.Json
  • Type.String

You can code a custom field type by yourself.

Here’s the template:

var Type = {};
Type.name = "type";
Type.needQuotes = false;    ///< Is this type need quotes in SQL statement?
Type.restore = function(v) {
    // v is a parsed value,
    // you should transform
    // it to the type that
    // SQL can recognize
    return v;
};
Type.parse = function(v) {
    // v is a original value,
    // you should parse it
    // into your own type
    return v;
};
Type.defaultValue = 0.1;    ///< Default value

You can refers to lib/fieldType/json.js to get more information.

Contribute

You’re welcome to pull requests!

Thanks to:

「雖然我覺得不怎麼可能有人會關注我」

13 回复

已使用sequelize

join mysql 有 join buffer、BAK 、 MRR 进行优化的,看你怎么使用索引与具体查询计划的
nodejs 还是使用 SQL mapping 比较靠谱 bearcat-dao

sequelize ORM实现的也不完善

对 orm 无好感。

以及,因为 mysql 本身效率就不是特别高 这个结论是跟哪款数据库比较得出的?

@alsotang 无好感,怎么成了精华贴,哈哈

@fantasyni 。。。精华不是根据我个人喜好来判断的。。

严格来说这不能算是 ORM,只是对数据层的一个封装。因为没有表间的关系映射,也不支持连表查询。因为 mysql 效率低下,所以有一层缓存层。

这个模型层封装原来是我针对花瓣内部原模型层与缓存层兼容进行的一次重构,后来我就抽离出来搞成更通用的了。mysql 是历史包袱,缓存和不支持低效查询是为了效率。

@fantasyni @alsotang

顺便,功能不强大不全,但是开发快,极简。

为毛Mysql是历史包袱啊?挺美好的东西,看你怎么用了。 目前我们淡化Model,面向前后端交互接口开发(这看上去更像是Controller),Model这层会根据Mysql的INFORMATION SCHEMA库自动生成对应的CRUDL,只需要指定一下Model用到的表和查询的视图。PS: Mysql的视图子查询还需改进。

它看上去会是这样: module.exports = { TABLE: TABLE_NAME, VIEW : VIEW_NAME, Create : { beforeCreate : function(…) {} afterCreate : function(…) {} }, Read : { event callbacks }, Update : { event callbacks }, Delete : { event callbacks }, List : { condition : { fieldA : ‘>’, fieldB : ‘=’, fieldC : ‘in’ //支持< > >= <= between likePrefix(xx%) likeSuffix(%xxx) in 等条件,各条件组合间是AND关系,支持运行时插入SQL片断和 }, order : { fieldA : ‘ASC’, fieldB : ‘DESC’ //支持自定义排序) }, event callbacks } };

这段定义框架会生成xxx.C, xxx.R, xxx.U, xxx.D, xxx.L等前后端交互接口(也可在后端直接调用)和文档(沟通成本很高哇),解决一些基本的增删改查功能,目前感觉还算便捷,功能也在逐渐完善中。

貌似因为readme里面的东西被顺路@了 toshihiko-redis,缓存层redis支持。 嘛就这样,顺路支持下楼主

事务怎么写?

事务怎么处理?

回到顶部