Sequelize 和 MySQL 對照

若是你以爲Sequelize的文檔有點多、雜,不方便看,能夠看看這篇。javascript

在使用NodeJS來關係型操做數據庫時,爲了方便,一般都會選擇一個合適的ORM(Object Relationship Model)框架。畢竟直接操做SQL比較繁瑣,經過ORM框架,咱們可使用面向對象的方式來操做表。NodeJS社區有不少的ORM框架,我比較喜歡Sequelize,它功能豐富,能夠很是方便的進行連表查詢。css

這篇文章咱們就來看看,Sequelize是如何在SQL之上進行抽象、封裝,從而提升開發效率的。java

安裝

這篇文章主要使用MySQLSequelizeco來進行介紹。安裝很是簡單:mysql

$ npm install --save co
$ npm install --save sequelize
$ npm install --save mysql

代碼模板以下:sql

var Sequelize = require('sequelize');
var co = require('co');

co(function* () {
    // code here
}).catch(function(e) {
    console.log(e);
});

基本上,Sequelize的操做都會返回一個Promise,在co的框架裏面能夠直接進行yield,很是方便。數據庫

創建數據庫鏈接

var sequelize = new Sequelize(
    'sample', // 數據庫名
    'root',   // 用戶名
    'zuki',   // 用戶密碼
    {
        'dialect': 'mysql',  // 數據庫使用mysql
        'host': 'localhost', // 數據庫服務器ip
        'port': 3306,        // 數據庫服務器端口
        'define': {
            // 字段如下劃線(_)來分割(默認是駝峯命名風格)
            'underscored': true
        }
    }
);

定義單張表

Sequelizenpm

var User = sequelize.define(
    // 默認表名(通常這裏寫單數),生成時會自動轉換成複數形式
    // 這個值還會做爲訪問模型相關的模型時的屬性名,因此建議用小寫形式
    'user',
    // 字段定義(主鍵、created_at、updated_at默認包含,不用特殊定義)
    {
        'emp_id': {
            'type': Sequelize.CHAR(10), // 字段類型
            'allowNull': false,         // 是否容許爲NULL
            'unique': true              // 字段是否UNIQUE
        },
        'nick': {
            'type': Sequelize.CHAR(10),
            'allowNull': false
        },
        'department': {
            'type': Sequelize.STRING(64),
            'allowNull': true
        }
    }
);

SQLapi

CREATE TABLE IF NOT EXISTS `users` (
    `id` INTEGER NOT NULL auto_increment , 
    `emp_id` CHAR(10) NOT NULL UNIQUE, 
    `nick` CHAR(10) NOT NULL, 
    `department` VARCHAR(64),
    `created_at` DATETIME NOT NULL, 
    `updated_at` DATETIME NOT NULL, 
    PRIMARY KEY (`id`)
) ENGINE=InnoDB;

幾點說明:數組

  1. 建表SQL會自動執行的意思是你主動調用sync的時候。相似這樣:User.sync({force: true});(加force:true,會先刪掉表後再建表)。咱們也能夠先定義好表結構,再來定義Sequelize模型,這時能夠不用sync。二者在定義階段沒有什麼關係,直到咱們真正開始操做模型時,纔會觸及到表的操做,可是咱們固然仍是要儘可能保證模型和表的同步(能夠藉助一些migration工具)。自動建表功能有風險,使用需謹慎。bash

  2. 全部數據類型,請參考文檔數據類型

  3. 模型還能夠定義虛擬屬性、類方法、實例方法,請參考文檔:模型定義

  4. 其餘一些特殊定義以下所示:

var User = sequelize.define(
    'user',
    {
        'emp_id': {
            'type': Sequelize.CHAR(10), // 字段類型
            'allowNull': false,         // 是否容許爲NULL
            'unique': true              // 字段是否UNIQUE
        },
        'nick': {
            'type': Sequelize.CHAR(10),
            'allowNull': false
        },
        'department': {
            'type': Sequelize.STRING(64),
            'allowNull': true
        }
    },
    {
        // 自定義表名
        'freezeTableName': true,
        'tableName': 'xyz_users',

        // 是否須要增長createdAt、updatedAt、deletedAt字段
        'timestamps': true,

        // 不須要createdAt字段
        'createdAt': false,

        // 將updatedAt字段改個名
        'updatedAt': 'utime'

        // 將deletedAt字段更名
        // 同時須要設置paranoid爲true(此種模式下,刪除數據時不會進行物理刪除,而是設置deletedAt爲當前時間
        'deletedAt': 'dtime',
        'paranoid': true
    }
);

單表增刪改查

經過Sequelize獲取的模型對象都是一個DAO(Data Access Object)對象,這些對象會擁有許多操做數據庫表的實例對象方法(好比:saveupdatedestroy等),須要獲取「乾淨」的JSON對象能夠調用get({'plain': true})

經過模型的類方法能夠獲取模型對象(好比:findByIdfindAll等)。

Sequelize

// 方法1:build後對象只存在於內存中,調用save後才操做db
var user = User.build({
    'emp_id': '1',
    'nick': '小紅',
    'department': '技術部'
});
user = yield user.save();
console.log(user.get({'plain': true}));

// 方法2:直接操做db
var user = yield User.create({
    'emp_id': '2',
    'nick': '小明',
    'department': '技術部'
});
console.log(user.get({'plain': true}));

SQL

INSERT INTO `users` 
(`id`, `emp_id`, `nick`, `department`, `updated_at`, `created_at`) 
VALUES 
(DEFAULT, '1', '小紅', '技術部', '2015-11-02 14:49:54', '2015-11-02 14:49:54');

Sequelize會爲主鍵id設置DEFAULT值來讓數據庫產生自增值,還將當前時間設置成了created_atupdated_at字段,很是方便。

Sequelize

// 方法1:操做對象屬性(不會操做db),調用save後操做db
user.nick = '小白';
user = yield user.save();
console.log(user.get({'plain': true}));

// 方法2:直接update操做db
user = yield user.update({
    'nick': '小白白'
});
console.log(user.get({'plain': true}));

SQL

UPDATE `users` 
SET `nick` = '小白白', `updated_at` = '2015-11-02 15:00:04' 
WHERE `id` = 1;

更新操做時,Sequelize將將當前時間設置成了updated_at,很是方便。

若是想限制更新屬性的白名單,能夠這樣寫:

// 方法1
user.emp_id = '33';
user.nick = '小白';
user = yield user.save({'fields': ['nick']});

// 方法2
user = yield user.update(
    {'emp_id': '33', 'nick': '小白'},
    {'fields': ['nick']}
});

這樣就只會更新nick字段,而emp_id會被忽略。這種方法在對錶單提交過來的一大推數據中只更新某些屬性的時候比較有用。

Sequelize

yield user.destroy();

SQL

DELETE FROM `users` WHERE `id` = 1;

這裏有個特殊的地方是,若是咱們開啓了paranoid(偏執)模式,destroy的時候不會執行DELETE語句,而是執行一個UPDATE語句將deleted_at字段設置爲當前時間(一開始此字段值爲NULL)。咱們可使用user.destroy({force: true})來強制刪除,從而執行DELETE語句進行物理刪除。

查所有

Sequelize

var users = yield User.findAll();
console.log(users);

SQL

SELECT `id`, `emp_id`, `nick`, `department`, `created_at`, `updated_at` FROM `users`;

限制字段

Sequelize

var users = yield User.findAll({
    'attributes': ['emp_id', 'nick']
});
console.log(users);

SQL

SELECT `emp_id`, `nick` FROM `users`;

字段重命名

Sequelize

var users = yield User.findAll({
    'attributes': [
        'emp_id', ['nick', 'user_nick']
    ]
});
console.log(users);

SQL

SELECT `emp_id`, `nick` AS `user_nick` FROM `users`;

where子句

Sequelizewhere配置項基本上徹底支持了SQLwhere子句的功能,很是強大。咱們一步步來進行介紹。

基本條件

Sequelize

var users = yield User.findAll({
    'where': {
        'id': [1, 2, 3],
        'nick': 'a',
        'department': null
    }
});
console.log(users);

SQL

SELECT `id`, `emp_id`, `nick`, `department`, `created_at`, `updated_at` 
FROM `users` AS `user` 
WHERE 
    `user`.`id` IN (1, 2, 3) AND 
    `user`.`nick`='a' AND 
    `user`.`department` IS NULL;

能夠看到,k: v被轉換成了k = v,同時一個對象的多個k: v對被轉換成了AND條件,即:k1: v1, k2: v2轉換爲k1 = v1 AND k2 = v2

這裏有2個要點:

  • 若是v是null,會轉換爲IS NULL(由於SQL沒有= NULL
    這種語法)

  • 若是v是數組,會轉換爲IN條件(由於SQL沒有=[1,2,3]這種語法,何況也沒數組這種類型)

操做符

操做符是對某個字段的進一步約束,能夠有多個(對同一個字段的多個操做符會被轉化爲AND)。

Sequelize

var users = yield User.findAll({
    'where': {
        'id': {
            '$eq': 1,                // id = 1
            '$ne': 2,                // id != 2

            '$gt': 6,                // id > 6
            '$gte': 6,               // id >= 6

            '$lt': 10,               // id < 10
            '$lte': 10,              // id <= 10

            '$between': [6, 10],     // id BETWEEN 6 AND 10
            '$notBetween': [11, 15], // id NOT BETWEEN 11 AND 15

            '$in': [1, 2],           // id IN (1, 2)
            '$notIn': [3, 4]         // id NOT IN (3, 4)
        },
        'nick': {
            '$like': '%a%',          // nick LIKE '%a%'
            '$notLike': '%a'         // nick NOT LIKE '%a'
        },
        'updated_at': {
            '$eq': null,             // updated_at IS NULL
            '$ne': null              // created_at IS NOT NULL
        }
    }
});

SQL

SELECT `id`, `emp_id`, `nick`, `department`, `created_at`, `updated_at` 
FROM `users` AS `user` 
WHERE 
(
    `user`.`id` = 1 AND 
    `user`.`id` != 2 AND 
    `user`.`id` > 6 AND 
    `user`.`id` >= 6 AND 
    `user`.`id` < 10 AND 
    `user`.`id` <= 10 AND 
    `user`.`id` BETWEEN 6 AND 10 AND 
    `user`.`id` NOT BETWEEN 11 AND 15 AND
    `user`.`id` IN (1, 2) AND 
    `user`.`id` NOT IN (3, 4)
) 
AND 
(
    `user`.`nick` LIKE '%a%' AND 
    `user`.`nick` NOT LIKE '%a'
) 
AND 
(
    `user`.`updated_at` IS NULL AND 
    `user`.`updated_at` IS NOT NULL
);

這裏咱們發現,其實相等條件k: v這種寫法是操做符寫法k: {$eq: v}的簡寫。而要實現不等條件就必須使用操做符寫法k: {$ne: v}

條件

上面咱們說的條件查詢,都是AND查詢,Sequelize同時也支持ORNOT、甚至多種條件的聯合查詢。

AND條件

Sequelize

var users = yield User.findAll({
    'where': {
        '$and': [
            {'id': [1, 2]},
            {'nick': null}
        ]
    }
});

SQL

SELECT `id`, `emp_id`, `nick`, `department`, `created_at`, `updated_at` 
FROM `users` AS `user` 
WHERE 
(
    `user`.`id` IN (1, 2) AND 
    `user`.`nick` IS NULL
);
OR條件

Sequelize

var users = yield User.findAll({
    'where': {
        '$or': [
            {'id': [1, 2]},
            {'nick': null}
        ]
    }
});

SQL

SELECT `id`, `emp_id`, `nick`, `department`, `created_at`, `updated_at` 
FROM `users` AS `user` 
WHERE 
(
    `user`.`id` IN (1, 2) OR 
    `user`.`nick` IS NULL
);
NOT條件

Sequelize

var users = yield User.findAll({
    'where': {
        '$not': [
            {'id': [1, 2]},
            {'nick': null}
        ]
    }
});

SQL

SELECT `id`, `emp_id`, `nick`, `department`, `created_at`, `updated_at` 
FROM `users` AS `user` 
WHERE 
NOT (
    `user`.`id` IN (1, 2) AND 
    `user`.`nick` IS NULL
);

轉換規則

咱們這裏作個總結。Sequelizewhere配置的轉換規則的僞代碼大概以下:

function translate(where) {

    for (k, v of where) {

        if (k == 表字段) {
            // 先統一轉爲操做符形式
            if (v == 基本值) { // k: 'xxx'
                v = {'$eq': v};
            }
            if (v == 數組) { // k: [1, 2, 3]
                v = {'$in': v};
            }

            // 操做符轉換
            for (opk, opv of v) {
                // op將opk轉換對應的SQL表示
                => k + op(opk, opv) + AND; 
            }
        }

        // 邏輯操做符處理

        if (k == '$and') {
            for (item in v) {
                => translate(item) + AND;
            }
        }

        if (k == '$or') {
            for (item in v) {
                => translate(item) + OR;
            }
        }

        if (k == '$not') {
            NOT +
            for (item in v) {
                => translate(item) + AND;
            }
        }

    }

    function op(opk, opv) {
        switch (opk) {
            case $eq => ('=' + opv) || 'IS NULL';
            case $ne => ('!=' + opv) || 'IS NOT NULL';
            case $gt => '>' + opv;
            case $lt => '<' + opv;
            case $gte => '>=' + opv;
            case $lte => '<=' + opv;
            case $between => 'BETWEEN ' + opv[0] + ' AND ' + opv[1];
            case $notBetween => 'NOT BETWEEN ' + opv[0] + ' AND ' + opv[1];
            case $in => 'IN (' + opv.join(',') + ')';
            case $notIn => 'NOT IN (' + opv.join(',') + ')';
            case $like => 'LIKE ' + opv;
            case $notLike => 'NOT LIKE ' + opv;
        }
    }

}

咱們看一個複雜例子,基本上就是按上述流程來進行轉換。

Sequelize

var users = yield User.findAll({
    'where': {
        'id': [3, 4],
        '$not': [
            {
                'id': {
                    '$in': [1, 2]
                }
            },
            {
                '$or': [
                    {'id': [1, 2]},
                    {'nick': null}
                ]
            }
        ],
        '$and': [
            {'id': [1, 2]},
            {'nick': null}
        ],
        '$or': [
            {'id': [1, 2]},
            {'nick': null}
        ]
    }
});

SQL

SELECT `id`, `emp_id`, `nick`, `department`, `created_at`, `updated_at` 
FROM `users` AS `user` 
WHERE 
    `user`.`id` IN (3, 4) 
AND 
NOT 
(
    `user`.`id` IN (1, 2) 
    AND 
    (`user`.`id` IN (1, 2) OR `user`.`nick` IS NULL)
)
AND 
(
    `user`.`id` IN (1, 2) AND `user`.`nick` IS NULL
) 
AND 
(
    `user`.`id` IN (1, 2) OR `user`.`nick` IS NULL
);

排序

Sequelize

var users = yield User.findAll({
    'order': [
        ['id', 'DESC'],
        ['nick']
    ]
});

SQL

SELECT `id`, `emp_id`, `nick`, `department`, `created_at`, `updated_at` 
FROM `users` AS `user` 
ORDER BY `user`.`id` DESC, `user`.`nick`;

分頁

Sequelize

var countPerPage = 20, currentPage = 5;
var users = yield User.findAll({
    'limit': countPerPage,                      // 每頁多少條
    'offset': countPerPage * (currentPage - 1)  // 跳過多少條
});

SQL

SELECT `id`, `emp_id`, `nick`, `department`, `created_at`, `updated_at` 
FROM `users` AS `user` 
LIMIT 80, 20;

其餘查詢方法

查詢一條數據

Sequelize

user = yield User.findById(1);

user = yield User.findOne({
    'where': {'nick': 'a'}
});

SQL

SELECT `id`, `emp_id`, `nick`, `department`, `created_at`, `updated_at` 
FROM `users` AS `user` 
WHERE `user`.`id` = 1 LIMIT 1;

SELECT `id`, `emp_id`, `nick`, `department`, `created_at`, `updated_at` 
FROM `users` AS `user` 
WHERE `user`.`nick` = 'a' LIMIT 1;
查詢並獲取數量

Sequelize

var result = yield User.findAndCountAll({
    'limit': 20,
    'offset': 0
});
console.log(result);

SQL

SELECT count(*) AS `count` FROM `users` AS `user`;

SELECT `id`, `emp_id`, `nick`, `department`, `created_at`, `updated_at` 
FROM `users` AS `user` 
LIMIT 20;

這個方法會執行2個SQL,返回的result對象將包含2個字段:result.count是數據總數,result.rows是符合查詢條件的全部數據。

批量操做

插入

Sequelize

var users = yield User.bulkCreate(
    [
        {'emp_id': 'a', 'nick': 'a'},
        {'emp_id': 'b', 'nick': 'b'},
        {'emp_id': 'c', 'nick': 'c'}
    ]
);

SQL

INSERT INTO `users` 
    (`id`,`emp_id`,`nick`,`created_at`,`updated_at`) 
VALUES 
    (NULL,'a','a','2015-11-03 02:43:30','2015-11-03 02:43:30'),
    (NULL,'b','b','2015-11-03 02:43:30','2015-11-03 02:43:30'),
    (NULL,'c','c','2015-11-03 02:43:30','2015-11-03 02:43:30');

這裏須要注意,返回的users數組裏面每一個對象的id值會是null。若是須要id值,能夠從新取下數據。

更新

Sequelize

var affectedRows = yield User.update(
    {'nick': 'hhhh'},
    {
        'where': {
            'id': [2, 3, 4]
        }
    }
);

SQL

UPDATE `users` 
SET `nick`='hhhh',`updated_at`='2015-11-03 02:51:05' 
WHERE `id` IN (2, 3, 4);

這裏返回的affectedRows實際上是一個數組,裏面只有一個元素,表示更新的數據條數(看起來像是Sequelize的一個bug)。

刪除

Sequelize

var affectedRows = yield User.destroy({
    'where': {'id': [2, 3, 4]}
});

SQL

DELETE FROM `users` WHERE `id` IN (2, 3, 4);

這裏返回的affectedRows是一個數字,表示刪除的數據條數。

關係

關係通常有三種:一對1、一對多、多對多。Sequelize提供了清晰易用的接口來定義關係、進行表間的操做。

當說到關係查詢時,通常會須要獲取多張表的數據。有建議用連表查詢join的,有不建議的。個人見解是,join查詢這種黑科技在數據量小的狀況下可使用,基本沒有什麼影響,數據量大的時候,join的性能可能會是硬傷,應該儘可能避免,能夠分別根據索引取單表數據而後在應用層對數據進行joinmerge。固然,查詢時必定要分頁,不要findAll

一對一

模型定義

Sequelize

var User = sequelize.define('user',
    {
        'emp_id': {
            'type': Sequelize.CHAR(10),
            'allowNull': false,
            'unique': true
        }
    }
);
var Account = sequelize.define('account',
    {
        'email': {
            'type': Sequelize.CHAR(20),
            'allowNull': false
        }
    }
);

/* 
 * User的實例對象將擁有getAccount、setAccount、addAccount方法
 */
User.hasOne(Account);
/*
 * Account的實例對象將擁有getUser、setUser、addUser方法
 */
Account.belongsTo(User);

SQL

CREATE TABLE IF NOT EXISTS `users` (
    `id` INTEGER NOT NULL auto_increment , 
    `emp_id` CHAR(10) NOT NULL UNIQUE, 
    `created_at` DATETIME NOT NULL, 
    `updated_at` DATETIME NOT NULL, 
    PRIMARY KEY (`id`)
) ENGINE=InnoDB;

CREATE TABLE IF NOT EXISTS `accounts` (
    `id` INTEGER NOT NULL auto_increment , 
    `email` CHAR(20) NOT NULL, 
    `created_at` DATETIME NOT NULL, 
    `updated_at` DATETIME NOT NULL, 
    `user_id` INTEGER, 
    PRIMARY KEY (`id`), 
    FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE SET NULL ON UPDATE CASCADE
) ENGINE=InnoDB;

能夠看到,這種關係中外鍵user_id加在了Account上。另外,Sequelize還給咱們生成了外鍵約束。

通常來講,外鍵約束在有些本身定製的數據庫系統裏面是禁止的,由於會帶來一些性能問題。因此,建表的SQL通常就去掉約束,同時給外鍵加一個索引(加速查詢),數據的一致性就靠應用層來保證了。

關係操做

Sequelize

var user = yield User.create({'emp_id': '1'});
var account = user.createAccount({'email': 'a'});
console.log(account.get({'plain': true}));

SQL

INSERT INTO `users` 
(`id`,`emp_id`,`updated_at`,`created_at`) 
VALUES 
(DEFAULT,'1','2015-11-03 06:24:53','2015-11-03 06:24:53');

INSERT INTO `accounts` 
(`id`,`email`,`user_id`,`updated_at`,`created_at`) 
VALUES 
(DEFAULT,'a',1,'2015-11-03 06:24:53','2015-11-03 06:24:53');

SQL執行邏輯是:

  • 使用對應的的user_id做爲外鍵在accounts表裏插入一條數據。

Sequelize

var anotherAccount = yield Account.create({'email': 'b'});
console.log(anotherAccount);
anotherAccount = yield user.setAccount(anotherAccount);
console.log(anotherAccount);

SQL

INSERT INTO `accounts` 
(`id`,`email`,`updated_at`,`created_at`) 
VALUES 
(DEFAULT,'b','2015-11-03 06:37:14','2015-11-03 06:37:14');

SELECT `id`, `email`, `created_at`, `updated_at`, `user_id` 
FROM `accounts` AS `account` WHERE (`account`.`user_id` = 1);

UPDATE `accounts` SET `user_id`=NULL,`updated_at`='2015-11-03 06:37:14' WHERE `id` = 1;
UPDATE `accounts` SET `user_id`=1,`updated_at`='2015-11-03 06:37:14' WHERE `id` = 2;

SQL執行邏輯是:

  • 插入一條account數據,此時外鍵user_id是空的,尚未關聯user

  • 找出當前user所關聯的account並將其user_id置爲`NUL(爲了保證一對一關係)

  • 設置新的acount的外鍵user_iduser的屬性id,生成關係

Sequelize

yield user.setAccount(null);

SQL

SELECT `id`, `email`, `created_at`, `updated_at`, `user_id` 
FROM `accounts` AS `account` 
WHERE (`account`.`user_id` = 1);

UPDATE `accounts` 
SET `user_id`=NULL,`updated_at`='2015-11-04 00:11:35' 
WHERE `id` = 1;

這裏的刪除實際上只是「切斷」關係,並不會真正的物理刪除記錄。

SQL執行邏輯是:

  • 找出user所關聯的account數據

  • 將其外鍵user_id設置爲NULL,完成關係的「切斷」

Sequelize

var account = yield user.getAccount();
console.log(account);

SQL

SELECT `id`, `email`, `created_at`, `updated_at`, `user_id` 
FROM `accounts` AS `account` 
WHERE (`account`.`user_id` = 1);

這裏就是調用usergetAccount方法,根據外鍵來獲取對應的account

可是其實咱們用面向對象的思惟來思考應該是獲取user的時候就能經過user.account的方式來訪問account對象。這能夠經過Sequelizeeager loading(急加載,和懶加載相反)來實現。

eager loading的含義是說,取一個模型的時候,同時也把相關的模型數據也給我取過來(我很着急,不能按默認那種取一個模型就取一個模型的方式,我還要更多)。方法以下:

Sequelize

var user = yield User.findById(1, {
    'include': [Account]
});
console.log(user.get({'plain': true}));
/* 
 * 輸出相似:
 { id: 1,
  emp_id: '1',
  created_at: Tue Nov 03 2015 15:25:27 GMT+0800 (CST),
  updated_at: Tue Nov 03 2015 15:25:27 GMT+0800 (CST),
  account:
   { id: 2,
     email: 'b',
     created_at: Tue Nov 03 2015 15:25:27 GMT+0800 (CST),
     updated_at: Tue Nov 03 2015 15:25:27 GMT+0800 (CST),
     user_id: 1 } }
 */

SQL

SELECT `user`.`id`, `user`.`emp_id`, `user`.`created_at`, `user`.`updated_at`, `account`.`id` AS `account.id`, `account`.`email` AS `account.email`, `account`.`created_at` AS `account.created_at`, `account`.`updated_at` AS `account.updated_at`, `account`.`user_id` AS `account.user_id` 
FROM `users` AS `user` LEFT OUTER JOIN `accounts` AS `account` 
ON `user`.`id` = `account`.`user_id` 
WHERE `user`.`id` = 1 LIMIT 1;

能夠看到,咱們對2個表進行了一個外聯接,從而在取user的同時也獲取到了account

其餘補充說明

若是咱們重複調用user.createAccount方法,實際上會在數據庫裏面生成多條user_id同樣的數據,並非真正的一對一。

因此,在應用層保證一致性時,就須要咱們遵循良好的編碼約定。新增就用user.createAccount,更改就用user.setAccount

也能夠給user_id加一個UNIQUE約束,在數據庫層面保證一致性,這時就須要作好try/catch,發生插入異常的時候可以知道是由於插入了多個account

另外,咱們上面都是使用user來對account進行操做。實際上反向操做也是能夠的,這是由於咱們定義了Account.belongsTo(User)。在Sequelize裏面定義關係時,關係的調用方會得到相關的「關係」方法,通常爲了兩邊都能操做,會同時定義雙向關係(這裏雙向關係指的是模型層面,並不會在數據庫表中出現兩個表都加上外鍵的狀況,請放心)。

一對多

模型定義

Sequelize

var User = sequelize.define('user',
    {
        'emp_id': {
            'type': Sequelize.CHAR(10),
            'allowNull': false,
            'unique': true
        }
    }
);
var Note = sequelize.define('note',
    {
        'title': {
            'type': Sequelize.CHAR(64),
            'allowNull': false
        }
    }
);

/*
 * User的實例對象將擁有getNotes、setNotes、addNote、createNote、removeNote、hasNote方法
 */
User.hasMany(Note);
/*
 * Note的實例對象將擁有getUser、setUser、createUser方法
 */
Note.belongsTo(User);

SQL

CREATE TABLE IF NOT EXISTS `users` (
    `id` INTEGER NOT NULL auto_increment , 
    `emp_id` CHAR(10) NOT NULL UNIQUE, 
    `created_at` DATETIME NOT NULL, 
    `updated_at` DATETIME NOT NULL, 
    PRIMARY KEY (`id`)
) ENGINE=InnoDB;

CREATE TABLE IF NOT EXISTS `notes` (
    `id` INTEGER NOT NULL auto_increment , 
    `title` CHAR(64) NOT NULL, 
    `created_at` DATETIME NOT NULL, 
    `updated_at` DATETIME NOT NULL, 
    `user_id` INTEGER, 
    PRIMARY KEY (`id`), 
    FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE SET NULL ON UPDATE CASCADE
) ENGINE=InnoDB;

能夠看到這種關係中,外鍵user_id加在了多的一端(notes表)。同時相關的模型也自動得到了一些方法。

關係操做

方法1

Sequelize

var user = yield User.create({'emp_id': '1'});
var note = yield user.createNote({'title': 'a'});
console.log(note);

SQL

NSERT INTO `users` 
(`id`,`emp_id`,`updated_at`,`created_at`) 
VALUES 
(DEFAULT,'1','2015-11-03 23:52:05','2015-11-03 23:52:05');

INSERT INTO `notes` 
(`id`,`title`,`user_id`,`updated_at`,`created_at`) 
VALUES 
(DEFAULT,'a',1,'2015-11-03 23:52:05','2015-11-03 23:52:05');

SQL執行邏輯:

  • 使用user的主鍵id值做爲外鍵直接在notes表裏插入一條數據。

方法2

Sequelize

var user = yield User.create({'emp_id': '1'});
var note = yield Note.create({'title': 'b'});
yield user.addNote(note);

SQL

INSERT INTO `users` 
(`id`,`emp_id`,`updated_at`,`created_at`) 
VALUES 
(DEFAULT,'1','2015-11-04 00:02:56','2015-11-04 00:02:56');

INSERT INTO `notes` 
(`id`,`title`,`updated_at`,`created_at`) 
VALUES 
(DEFAULT,'b','2015-11-04 00:02:56','2015-11-04 00:02:56');

UPDATE `notes` 
SET `user_id`=1,`updated_at`='2015-11-04 00:02:56' 
WHERE `id` IN (1);

SQL執行邏輯:

  • 插入一條note數據,此時該條數據的外鍵user_id爲空

  • 使用user的屬性id值再更新該條note數據,設置好外鍵,完成關係創建

Sequelize

// 爲user增長note一、note2
var user = yield User.create({'emp_id': '1'});
var note1 = yield user.createNote({'title': 'a'});
var note2 = yield user.createNote({'title': 'b'});
// 先建立note三、note4
var note3 = yield Note.create({'title': 'c'});
var note4 = yield Note.create({'title': 'd'});
// user擁有的note更改成note三、note4
yield user.setNotes([note3, note4]);

SQL

/* 省去了建立語句 */
SELECT `id`, `title`, `created_at`, `updated_at`, `user_id` 
FROM `notes` AS `note` WHERE `note`.`user_id` = 1;

UPDATE `notes` 
SET `user_id`=NULL,`updated_at`='2015-11-04 12:45:12' 
WHERE `id` IN (1, 2);

UPDATE `notes` 
SET `user_id`=1,`updated_at`='2015-11-04 12:45:12' 
WHERE `id` IN (3, 4);

SQL執行邏輯:

  • 根據user的屬性id查詢全部相關的note數據

  • note1note2的外鍵user_id置爲NULL,切斷關係

  • note3note4的外鍵user_id置爲user的屬性id,完成關係創建

這裏爲啥還要查出全部的note數據呢?由於咱們須要根據傳人setNotes的數組來計算出哪些note要切斷關係、哪些要新增關係,因此就須要查出來進行一個計算集合的「交集」運算。

Sequelize

var user = yield User.create({'emp_id': '1'});
var note1 = yield user.createNote({'title': 'a'});
var note2 = yield user.createNote({'title': 'b'});
yield user.setNotes([]);

SQL

SELECT `id`, `title`, `created_at`, `updated_at`, `user_id` 
FROM `notes` AS `note` WHERE `note`.`user_id` = 1;

UPDATE `notes` 
SET `user_id`=NULL,`updated_at`='2015-11-04 12:50:08' 
WHERE `id` IN (1, 2);

實際上,上面說到的「改」已經有「刪」的操做了(去掉note1note2的關係)。這裏的操做是刪掉用戶的全部note數據,直接執行user.setNotes([])便可。

SQL執行邏輯:

  • 根據user的屬性id查出全部相關的note數據

  • 將其外鍵user_id置爲NULL,切斷關係

還有一個真正的刪除方法,就是removeNote。以下所示:

Sequelize

yield user.removeNote(note);

SQL

UPDATE `notes` 
SET `user_id`=NULL,`updated_at`='2015-11-06 01:40:12' 
WHERE `user_id` = 1 AND `id` IN (1);

狀況1

查詢user的全部知足條件的note數據。

Sequelize

var notes = yield user.getNotes({
    'where': {
        'title': {
            '$like': '%css%'
        }
    }
});
notes.forEach(function(note) {
    console.log(note);
});

SQL

SELECT `id`, `title`, `created_at`, `updated_at`, `user_id` 
FROM `notes` AS `note` 
WHERE (`note`.`user_id` = 1 AND `note`.`title` LIKE '%a%');

這種方法的SQL很簡單,直接根據userid值來查詢知足條件的note便可。

狀況2

查詢全部知足條件的note,同時獲取note屬於哪一個user

Sequelize

var notes = yield Note.findAll({
    'include': [User],
    'where': {
        'title': {
            '$like': '%css%'
        }
    }
});
notes.forEach(function(note) {
    // note屬於哪一個user能夠經過note.user訪問
    console.log(note);
});

SQL

SELECT `note`.`id`, `note`.`title`, `note`.`created_at`, `note`.`updated_at`, `note`.`user_id`, 
`user`.`id` AS `user.id`, `user`.`emp_id` AS `user.emp_id`, `user`.`created_at` AS `user.created_at`, `user`.`updated_at` AS `user.updated_at` 
FROM `notes` AS `note` LEFT OUTER JOIN `users` AS `user` 
ON `note`.`user_id` = `user`.`id`
WHERE `note`.`title` LIKE '%css%';

這種方法,由於獲取的主體是note,因此將notesleft joinusers

狀況3

查詢全部知足條件的user,同時獲取該user全部知足條件的note

Sequelize

var users = yield User.findAll({
    'include': [Note],
    'where': {
        'created_at': {
            '$lt': new Date()
        }
    }
});
users.forEach(function(user) {
    // user的notes能夠經過user.notes訪問
    console.log(user); 
});

SQL

SELECT `user`.`id`, `user`.`emp_id`, `user`.`created_at`, `user`.`updated_at`, 
`notes`.`id` AS `notes.id`, `notes`.`title` AS `notes.title`, `notes`.`created_at` AS `notes.created_at`, `notes`.`updated_at` AS `notes.updated_at`, `notes`.`user_id` AS `notes.user_id` 
FROM `users` AS `user` LEFT OUTER JOIN `notes` AS `notes` 
ON `user`.`id` = `notes`.`user_id`
WHERE `user`.`created_at` < '2015-11-05 01:51:35';

這種方法獲取的主體是user,因此將usersleft joinnotes

一點補充

關於各類join的區別,能夠參考:http://blog.codinghorror.com/a-visual-explanation-of-sql-joins/

關於eager loading我想再囉嗦幾句。include裏面傳遞的是去取相關模型,默認是取所有,咱們也能夠再對這個模型進行一層過濾。像下面這樣:

Sequelize

// 查詢建立時間在今天以前的全部user,同時獲取他們note的標題中含有關鍵字css的全部note
var users = yield User.findAll({
    'include': [
        {
            'model': Note,
            'where': {
                'title': {
                    '$like': '%css%'
                }
            }
        }
    ],
    'where': {
        'created_at': {
            '$lt': new Date()
        }
    }
});

SQL

SELECT `user`.`id`, `user`.`emp_id`, `user`.`created_at`, `user`.`updated_at`, 
`notes`.`id` AS `notes.id`, `notes`.`title` AS `notes.title`, `notes`.`created_at` AS `notes.created_at`, `notes`.`updated_at` AS `notes.updated_at`, `notes`.`user_id` AS `notes.user_id` 
FROM `users` AS `user` INNER JOIN `notes` AS `notes` 
ON `user`.`id` = `notes`.`user_id` AND `notes`.`title` LIKE '%css%' 
WHERE `user`.`created_at` < '2015-11-05 01:58:31';

注意:當咱們對include的模型加了where過濾時,會使用inner join來進行查詢,這樣保證只有那些擁有標題含有css關鍵詞note的用戶纔會返回。

多對多關係

在多對多關係中,必需要額外一張關係表來將2個表進行關聯,這張表能夠是單純的一個關係表,也能夠是一個實際的模型(含有本身的額外屬性來描述關係)。我比較喜歡用一個模型的方式,這樣方便之後作擴展。

模型定義

Sequelize

var Note = sequelize.define('note',
    {
        'title': {
            'type': Sequelize.CHAR(64),
            'allowNull': false
        }
    }
);
var Tag = sequelize.define('tag',
    {
        'name': {
            'type': Sequelize.CHAR(64),
            'allowNull': false,
            'unique': true
        }
    }
);
var Tagging = sequelize.define('tagging',
    {
        'type': {
            'type': Sequelize.INTEGER(),
            'allowNull': false
        }
    }
);

// Note的實例擁有getTags、setTags、addTag、addTags、createTag、removeTag、hasTag方法
Note.belongsToMany(Tag, {'through': Tagging});
// Tag的實例擁有getNotes、setNotes、addNote、addNotes、createNote、removeNote、hasNote方法
Tag.belongsToMany(Note, {'through': Tagging});

SQL

CREATE TABLE IF NOT EXISTS `notes` (
    `id` INTEGER NOT NULL auto_increment , 
    `title` CHAR(64) NOT NULL, 
    `created_at` DATETIME NOT NULL, 
    `updated_at` DATETIME NOT NULL, 
    PRIMARY KEY (`id`)
) ENGINE=InnoDB;

CREATE TABLE IF NOT EXISTS `tags` (
    `id` INTEGER NOT NULL auto_increment , 
    `name` CHAR(64) NOT NULL UNIQUE, 
    `created_at` DATETIME NOT NULL, 
    `updated_at` DATETIME NOT NULL, 
    PRIMARY KEY (`id`)
) ENGINE=InnoDB;

CREATE TABLE IF NOT EXISTS `taggings` (
    `type` INTEGER NOT NULL, 
    `created_at` DATETIME NOT NULL, 
    `updated_at` DATETIME NOT NULL, 
    `tag_id` INTEGER , 
    `note_id` INTEGER , 
    PRIMARY KEY (`tag_id`, `note_id`), 
    FOREIGN KEY (`tag_id`) REFERENCES `tags` (`id`) ON DELETE CASCADE ON UPDATE CASCADE, 
    FOREIGN KEY (`note_id`) REFERENCES `notes` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB;

能夠看到,多對多關係中單獨生成了一張關係表,並設置了2個外鍵tag_idnote_id來和tagsnotes進行關聯。關於關係表的命名,我比較喜歡使用動詞,由於這張表是用來表示兩張表的一種聯繫,並且這種聯繫多數時候伴隨着一種動做。好比:用戶收藏商品(collecting)、用戶購買商品(buying)、用戶加入項目(joining)等等。

方法1

Sequelize

var note = yield Note.create({'title': 'note'});
yield note.createTag({'name': 'tag'}, {'type': 0});

SQL

INSERT INTO `notes` 
(`id`,`title`,`updated_at`,`created_at`) 
VALUES 
(DEFAULT,'note','2015-11-06 02:14:38','2015-11-06 02:14:38');

INSERT INTO `tags` 
(`id`,`name`,`updated_at`,`created_at`) 
VALUES 
(DEFAULT,'tag','2015-11-06 02:14:38','2015-11-06 02:14:38');

INSERT INTO `taggings` 
(`tag_id`,`note_id`,`type`,`created_at`,`updated_at`) 
VALUES 
(1,1,0,'2015-11-06 02:14:38','2015-11-06 02:14:38');

SQL執行邏輯:

  • notes表插入記錄

  • tags表中插入記錄

  • 使用對應的值設置外鍵tag_idnote_id以及關係模型自己須要的屬性(type: 0)在關係表tagging中插入記錄

關係表自己須要的屬性,經過傳遞一個額外的對象給設置方法來實現。

方法2

Sequelize

var note = yield Note.create({'title': 'note'});
var tag = yield Tag.create({'name': 'tag'});
yield note.addTag(tag, {'type': 1});

SQL

INSERT INTO `notes` 
(`id`,`title`,`updated_at`,`created_at`) 
VALUES 
(DEFAULT,'note','2015-11-06 02:20:52','2015-11-06 02:20:52');

INSERT INTO `tags` 
(`id`,`name`,`updated_at`,`created_at`) 
VALUES 
(DEFAULT,'tag','2015-11-06 02:20:52','2015-11-06 02:20:52');

INSERT INTO `taggings` 
(`tag_id`,`note_id`,`type`,`created_at`,`updated_at`) 
VALUES 
(1,1,1,'2015-11-06 02:20:52','2015-11-06 02:20:52');

這種方法和上面的方法其實是同樣的。只是咱們先手動create了一個Tag模型。

方法3

Sequelize

var note = yield Note.create({'title': 'note'});
var tag1 = yield Tag.create({'name': 'tag1'});
var tag2 = yield Tag.create({'name': 'tag2'});
yield note.addTags([tag1, tag2], {'type': 2});

SQL

INSERT INTO `notes` 
(`id`,`title`,`updated_at`,`created_at`) 
VALUES 
(DEFAULT,'note','2015-11-06 02:25:18','2015-11-06 02:25:18');

INSERT INTO `tags` 
(`id`,`name`,`updated_at`,`created_at`) 
VALUES 
(DEFAULT,'tag1','2015-11-06 02:25:18','2015-11-06 02:25:18');

INSERT INTO `tags` 
(`id`,`name`,`updated_at`,`created_at`) 
VALUES 
(DEFAULT,'tag2','2015-11-06 02:25:18','2015-11-06 02:25:18');

INSERT INTO `taggings` (`tag_id`,`note_id`,`type`,`created_at`,`updated_at`) 
VALUES 
(1,1,2,'2015-11-06 02:25:18','2015-11-06 02:25:18'),
(2,1,2,'2015-11-06 02:25:18','2015-11-06 02:25:18');

這種方法能夠進行批量添加。當執行addTags時,實際上就是設置好對應的外鍵及關係模型自己的屬性,而後在關係表中批量的插入數據。

Sequelize

// 先添加幾個tag
var note = yield Note.create({'title': 'note'});
var tag1 = yield Tag.create({'name': 'tag1'});
var tag2 = yield Tag.create({'name': 'tag2'});
yield note.addTags([tag1, tag2], {'type': 2});
// 將tag改掉
var tag3 = yield Tag.create({'name': 'tag3'});
var tag4 = yield Tag.create({'name': 'tag4'});
yield note.setTags([tag3, tag4], {'type': 3});

SQL

/* 前面添加部分的sql,和上面同樣*/
INSERT INTO `notes` 
(`id`,`title`,`updated_at`,`created_at`) 
VALUES 
(DEFAULT,'note','2015-11-06 02:25:18','2015-11-06 02:25:18');

INSERT INTO `tags` 
(`id`,`name`,`updated_at`,`created_at`) 
VALUES 
(DEFAULT,'tag1','2015-11-06 02:25:18','2015-11-06 02:25:18');

INSERT INTO `tags` 
(`id`,`name`,`updated_at`,`created_at`) 
VALUES 
(DEFAULT,'tag2','2015-11-06 02:25:18','2015-11-06 02:25:18');

INSERT INTO `taggings` 
(`tag_id`,`note_id`,`type`,`created_at`,`updated_at`) 
VALUES 
(1,1,2,'2015-11-06 02:25:18','2015-11-06 02:25:18'),
(2,1,2,'2015-11-06 02:25:18','2015-11-06 02:25:18');

/* 更改部分的sql */
INSERT INTO `tags` 
(`id`,`name`,`updated_at`,`created_at`) 
VALUES 
(DEFAULT,'tag3','2015-11-06 02:29:55','2015-11-06 02:29:55');

INSERT INTO `tags` 
(`id`,`name`,`updated_at`,`created_at`) 
VALUES 
(DEFAULT,'tag4','2015-11-06 02:29:55','2015-11-06 02:29:55');

/* 先刪除關係 */
DELETE FROM `taggings` 
WHERE `note_id` = 1 AND `tag_id` IN (1, 2);

/* 插入新關係 */
INSERT INTO `taggings` 
(`tag_id`,`note_id`,`type`,`created_at`,`updated_at`) 
VALUES 
(3,1,3,'2015-11-06 02:29:55','2015-11-06 02:29:55'),
(4,1,3,'2015-11-06 02:29:55','2015-11-06 02:29:55');

執行邏輯是,先將tag1tag2在關係表中的關係刪除,而後再將tag3tag4對應的關係插入關係表。

Sequelize

// 先添加幾個tag
var note = yield Note.create({'title': 'note'});
var tag1 = yield Tag.create({'name': 'tag1'});
var tag2 = yield Tag.create({'name': 'tag2'});
var tag3 = yield Tag.create({'name': 'tag2'});
yield note.addTags([tag1, tag2, tag3], {'type': 2});

// 刪除一個
yield note.removeTag(tag1);

// 所有刪除
yield note.setTags([]);

SQL

/* 刪除一個 */
DELETE FROM `taggings` WHERE `note_id` = 1 AND `tag_id` IN (1);

/* 刪除所有 */
SELECT `type`, `created_at`, `updated_at`, `tag_id`, `note_id` 
FROM `taggings` AS `tagging` 
WHERE `tagging`.`note_id` = 1;

DELETE FROM `taggings` WHERE `note_id` = 1 AND `tag_id` IN (2, 3);

刪除一個很簡單,直接將關係表中的數據刪除。

所有刪除時,首先須要查出關係表中note_id對應的全部數據,而後一次刪掉。

狀況1

查詢note全部知足條件的tag

Sequelize

var tags = yield note.getTags({
    //這裏能夠對tags進行where
});
tags.forEach(function(tag) {
    // 關係模型能夠經過tag.tagging來訪問
    console.log(tag);
});

SQL

SELECT `tag`.`id`, `tag`.`name`, `tag`.`created_at`, `tag`.`updated_at`, 
`tagging`.`type` AS `tagging.type`, `tagging`.`created_at` AS `tagging.created_at`, `tagging`.`updated_at` AS `tagging.updated_at`, `tagging`.`tag_id` AS `tagging.tag_id`, `tagging`.`note_id` AS `tagging.note_id` 
FROM `tags` AS `tag` 
INNER JOIN `taggings` AS `tagging` 
ON 
`tag`.`id` = `tagging`.`tag_id` AND `tagging`.`note_id` = 1;

能夠看到這種查詢,就是執行一個inner join

狀況2

查詢全部知足條件的tag,同時獲取每一個tag所在的note

Sequelize

var tags = yield Tag.findAll({
    'include': [
        {
            'model': Note
            // 這裏能夠對notes進行where
        }
    ]
    // 這裏能夠對tags進行where
});
tags.forEach(function(tag) {
    // tag的notes能夠經過tag.notes訪問,關係模型能夠經過tag.notes[0].tagging訪問
    console.log(tag); 
});

SQL

SELECT `tag`.`id`, `tag`.`name`, `tag`.`created_at`, `tag`.`updated_at`, 
`notes`.`id` AS `notes.id`, `notes`.`title` AS `notes.title`, `notes`.`created_at` AS `notes.created_at`, `notes`.`updated_at` AS `notes.updated_at`, 
`notes.tagging`.`type` AS `notes.tagging.type`, `notes.tagging`.`created_at` AS `notes.tagging.created_at`, `notes.tagging`.`updated_at` AS `notes.tagging.updated_at`, `notes.tagging`.`tag_id` AS `notes.tagging.tag_id`, `notes.tagging`.`note_id` AS `notes.tagging.note_id` 
FROM `tags` AS `tag` 
LEFT OUTER JOIN 
(
    `taggings` AS `notes.tagging` INNER JOIN `notes` AS `notes` 
    ON 
    `notes`.`id` = `notes.tagging`.`note_id`
) 
ON `tag`.`id` = `notes.tagging`.`tag_id`;

這個查詢就稍微有點複雜。首先是notestaggings進行了一個inner join,選出notes;而後tags和剛join出的集合再作一次left join,獲得結果。

狀況3

查詢全部知足條件的note,同時獲取每一個note全部知足條件的tag

Sequelize

var notes = yield Note.findAll({
    'include': [
        {
            'model': Tag
            // 這裏能夠對tags進行where
        }
    ]
    // 這裏能夠對notes進行where
});
notes.forEach(function(note) {
    // note的tags能夠經過note.tags訪問,關係模型經過note.tags[0].tagging訪問
    console.log(note);
});

SQL

SELECT 
`note`.`id`, `note`.`title`, `note`.`created_at`, `note`.`updated_at`, 
`tags`.`id` AS `tags.id`, `tags`.`name` AS `tags.name`, `tags`.`created_at` AS `tags.created_at`, `tags`.`updated_at` AS `tags.updated_at`, 
`tags.tagging`.`type` AS `tags.tagging.type`, `tags.tagging`.`created_at` AS `tags.tagging.created_at`, `tags.tagging`.`updated_at` AS `tags.tagging.updated_at`, `tags.tagging`.`tag_id` AS `tags.tagging.tag_id`, `tags.tagging`.`note_id` AS `tags.tagging.note_id` 
FROM `notes` AS `note` 
LEFT OUTER JOIN 
(
    `taggings` AS `tags.tagging` INNER JOIN `tags` AS `tags` 
    ON 
    `tags`.`id` = `tags.tagging`.`tag_id`
) 
ON 
`note`.`id` = `tags.tagging`.`note_id`;

這個查詢和上面的查詢相似。首先是tagstaggins進行了一個inner join,選出tags;而後notes和剛join出的集合再作一次left join,獲得結果。

其餘沒有涉及東西

這篇文章已經夠長了,可是其實咱們還有不少沒有涉及的東西,好比:聚合函數及查詢(havinggroup by)、模型的驗證(validate)、定義鉤子(hooks)、索引等等。

這些主題下次再來寫寫。

相關文章
相關標籤/搜索