V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
XadillaX
V2EX  ›  Node.js

一个自用的极简 ORM,带三方缓存支持——Toshihiko

  •  1
     
  •   XadillaX · 2015-03-06 13:47:45 +08:00 · 3922 次点击
    这是一个创建于 3340 天前的主题,其中的信息可能已经有所发展或是发生改变。

    目前花瓣网和大搜车都有项目在用该 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](lib/fieldType/json.js) to get more information.

    Contribute

    You're welcome to pull requests!

    Thanks to:

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

    9 条回复    2015-03-06 16:28:06 +08:00
    Shared
        1
    Shared  
       2015-03-06 13:52:31 +08:00   ❤️ 1
    @livid 快来修 CSS
    Livid
        2
    Livid  
    MOD
       2015-03-06 14:11:55 +08:00 via iPhone
    @Shared 收到。。。
    phoenixlzx
        4
    phoenixlzx  
       2015-03-06 14:37:02 +08:00   ❤️ 1
    诶我也想 @Livid 来着

    另外给菊苣点个赞
    XadillaX
        5
    XadillaX  
    OP
       2015-03-06 14:50:29 +08:00
    其实我这边用了别人写的 CSS,但是貌似也很难看 0. 0
    chunyang
        6
    chunyang  
       2015-03-06 15:14:32 +08:00   ❤️ 1
    // off-topic: 不用把整个 README 都粘贴过,链接很方便点。
    XadillaX
        7
    XadillaX  
    OP
       2015-03-06 15:20:30 +08:00
    @chunyang 其实我只是想简单介绍下用法给大家快看,然后又实在懒得再写一份了 -。 -
    RIcter
        8
    RIcter  
       2015-03-06 15:26:06 +08:00   ❤️ 1
    虽然指定了 key1 是 Int,但是好像没有强制转换的样子?

    Model.where({"key1": "1) union select 1,user(),3#"}).find(function(err, rows){console.log(rows[0]['key2']);});

    返回 root@localhost
    XadillaX
        9
    XadillaX  
    OP
       2015-03-06 16:28:06 +08:00
    @RIcter 给大神跪了!然后蒟蒻默默把它修了 QAQ
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   我们的愿景   ·   实用小工具   ·   1087 人在线   最高记录 6543   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 46ms · UTC 23:18 · PVG 07:18 · LAX 16:18 · JFK 19:18
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.