使用Sequelize連線資料庫

瘋狂紫蕭2018發表於2018-08-09

Sequelize.js是一款針對nodejs的ORM框架。

使用nodejs連線過資料庫的人肯定對資料庫不陌生了。如果是直接連結,需要自己建立並管理連線,還需要手動編寫sql語句。簡單的專案到是無所謂,可是一旦專案設計的東西比較複雜,表比較多的時候整個sql的編寫就非常的消耗精力。

在Java、c#等語言中已經有輕量的資料庫框架或者解決方案了。在nodejs中我推薦Sequelize。它是一個很成熟的框架,在速度和效能上也非常有優勢。而其中最關鍵的地方就在於,日常開發只需要管理物件的建立、查詢方法的呼叫等即可,極少需要編寫sql語句。這一個好處就是省去了複雜的sql語句維護,同時也避免了因sql而引起的不必要的bug。

Sequelize是針對node.js和io.js提供的ORM框架。具體就是突出一個支援廣泛,配置和查詢方法統一。它支援的資料庫包括:PostgreSQL、 MySQL、MariaDB、 SQLite 和 MSSQL。

本文中測試以及API展示地址:github地址

演示

Sequelize的呼叫突出一個簡單快捷。具體情況可以感受一下下面的程式碼。如果有過開發經驗的可以略過。

Table1.findById(23);
//select a,b,c,d from table1 where id=23;

Table1.findAll({
	where:{a:"test",b:76}
});
//select a,b,c,d from table1 where a="test" and "b=76;
複製程式碼

在單表查詢的時候只需要簡單的配置即可完成查詢。是不是非常的簡單方便呢?

連線資料庫

Sequelize的連線需要傳入引數,並且可以配置開啟執行緒池、讀寫分庫等操作。

簡單的寫法是這樣的:new Sequelize("表名","使用者名稱","密碼",配置)

正常使用中很少使用到所有的引數,這裡提供一個常用的模板,只需要修改自己使用的值即可。

const sequelize = new Sequelize('database', 'username', 'password',  {
  host: 'localhost',    //資料庫地址,預設本機
  port:'3306',
  dialect: 'mysql',
  pool: {   //連線池設定
    max: 5, //最大連線數
    min: 0, //最小連線數
    idle: 10000
  },
 });
複製程式碼

下面是詳細的配置引數。

const sequelize = new Sequelize('database', 'username', 'password', {
  // 資料庫型別,支援: 'mysql', 'sqlite', 'postgres', 'mssql'
  dialect: 'mysql',
  // 自定義連結地址,可以是ip或者域名,預設本機:localhost
  host: 'my.server.tld',
  // 自定義埠,預設3306
  port: 12345,
  // postgres使用的引數,連線型別,預設:tcp
  protocol: null,
  // 是否開始日誌,預設是用console.log
  // 建議開啟,方便對照生成的sql語句
  logging: true,
  // 預設是空
  // 支援: 'mysql', 'postgres', 'mssql'
  dialectOptions: {
    socketPath: '/Applications/MAMP/tmp/mysql/mysql.sock',
    supportBigNumbers: true,
    bigNumberStrings: true
  },
  // sqlite的儲存位置,僅sqlite有用
  // - 預設 ':memory:'
  storage: 'path/to/database.sqlite',

  // 是否將undefined轉化為NULL
  // - 預設: false
  omitNull: true,
  // pg中開啟ssl支援
  // - 預設: false
  native: true,
  // 資料庫預設引數,全域性引數
  define: {
    underscored: false
    freezeTableName: false,
    charset: 'utf8',
    dialectOptions: {
      collate: 'utf8_general_ci'
    },
    timestamps: true
  },
  // 是否同步
  sync: { force: true },
  // 連線池配置
  pool: {
    max: 5,
    idle: 30000,
    acquire: 60000,
  },
  isolationLevel: Transaction.ISOLATION_LEVELS.REPEATABLE_READ
})
複製程式碼

定義模型物件

在使用之前一定要先建立模型物件。就是資料庫中表的名稱、使用到的欄位、欄位型別等。

這裡有一個推薦的開發方式。先在nodejs中將物件建立出來,然後呼叫Sequelize的同步方法,將資料庫自動建立出來。這樣就避免了既要寫程式碼建表,又要手工建立資料庫中的表的操作。只需要單獨考慮程式碼中的物件型別等屬性就好了。

如果資料庫中已經建好了表,並且不能刪除,這個時候就不能自動建立了,因為建立的時候會刪除掉舊的資料。

下面是簡單的物件建立多數情況下這樣就可以了。


const users = db.define('t_user'/*自定義表名*/, {
    id: {
        type: Sequelize.INTEGER,
        primaryKey: true,       //主鍵
        autoIncrement: true,    //自增
        comment: "自增id"       //註釋:只在程式碼中有效
    },
    //使用者名稱
    username: {
        type: Sequelize.STRING,
        validate:{
	        isEmail: true,   //型別檢測,是否是郵箱格式
        }
    },
    //密碼
    pwd: {
        type: Sequelize.STRING(10),
        allowNull: false,//不允許為null
    },
    //狀態
    status: {
        type: Sequelize.INTEGER,
         defaultValue: 0,//預設值是0
    },
    //暱稱
    nickname: {
        type: Sequelize.STRING
    },
    //token
    token: {
        type: Sequelize.UUID
    },
    create_time: {
        type: Sequelize.DATE,
        defaultValue: Sequelize.NOW
    }
}, {
    //使用自定義表名
    freezeTableName: true,
    //去掉預設的新增時間和更新時間
    timestamps: false,
    indexes:[
	    //普通索引,預設BTREE
        {
            unique: true,
            fields: ['pid']
        },
     ]
});

//同步:沒有就新建,有就不變
// users.sync();
//先刪除後同步
users.sync({
    force: true
});
複製程式碼

資料型別

前段將了物件的建立,裡面用到了物件的各種型別。這裡再介紹一下型別的具體使用方式。

Sequelize.STRING 		//字串,長度預設255,VARCHAR(255)
Sequelize.STRING(1234)  //設定長度的字串,VARCHAR(1234)
Sequelize.STRING.BINARY   //定義型別VARCHAR BINARY
Sequelize.TEXT           //長字串,文字 TEXT
Sequelize.TEXT('tiny')   //小文字字串,TINYTEXT

Sequelize.INTEGER      //int數字,int
Sequelize.BIGINT       //更大的數字,BIGINT
Sequelize.BIGINT(11)   //設定長度的數字,BIGINT(11)

Sequelize.FLOAT        //浮點型別,FLOAT
Sequelize.FLOAT(11)     //設定長度的浮點,FLOAT(11)
Sequelize.FLOAT(11, 12)  //設定長度和小數位數的浮點,FLOAT(11,12)

Sequelize.REAL     //REAL  PostgreSQL only.
Sequelize.REAL(11) // REAL(11)    PostgreSQL only.
Sequelize.REAL(11, 12)  // REAL(11,12) PostgreSQL only.

Sequelize.DOUBLE     // DOUBLE
Sequelize.DOUBLE(11)  // DOUBLE(11)
Sequelize.DOUBLE(11, 12) // DOUBLE(11,12)

Sequelize.DECIMAL     // DECIMAL
Sequelize.DECIMAL(10, 2)  // DECIMAL(10,2)

Sequelize.DATE    // 日期型別,DATETIME for mysql / sqlite, TIMESTAMP WITH TIME ZONE for postgres
Sequelize.DATE(6) // mysql 5.6.4+支援,分秒精度為6位
Sequelize.DATEONLY   // 僅日期部分
Sequelize.BOOLEAN   // int型別,長度為1,TINYINT(1)

Sequelize.ENUM('value 1', 'value 2')  // 列舉型別
Sequelize.ARRAY(Sequelize.TEXT)  //PostgreSQL only.
Sequelize.ARRAY(Sequelize.ENUM)  //  PostgreSQL only.

Sequelize.JSON   // JSON column. PostgreSQL, SQLite and MySQL only.
Sequelize.JSONB  // JSONB column. PostgreSQL only.

Sequelize.BLOB   // BLOB (bytea for PostgreSQL)
Sequelize.BLOB('tiny')  // TINYBLOB (bytea for PostgreSQL. Other options are medium and long)

Sequelize.UUID  // PostgreSQL和SQLite的資料型別是UUID, MySQL是CHAR(36)型別

Sequelize.CIDR  // PostgreSQL中的CIDR型別
Sequelize.INET   // PostgreSQL中的INET型別
Sequelize.MACADDR  // PostgreSQL中的MACADDR型別

Sequelize.RANGE(Sequelize.INTEGER)    //PostgreSQL only.
Sequelize.RANGE(Sequelize.BIGINT)     // PostgreSQL only.
Sequelize.RANGE(Sequelize.DATE)       //PostgreSQL only.
Sequelize.RANGE(Sequelize.DATEONLY)   //PostgreSQL only.
Sequelize.RANGE(Sequelize.DECIMAL)    //PostgreSQL only.

Sequelize.ARRAY(Sequelize.RANGE(Sequelize.DATE)) // PostgreSQL only.

Sequelize.GEOMETRY   //PostgreSQL (with PostGIS) or MySQL only.
Sequelize.GEOMETRY('POINT')  // PostgreSQL (with PostGIS) or MySQL only.
Sequelize.GEOMETRY('POINT', 4326)// PostgreSQL (with PostGIS) or MySQL only.
複製程式碼

資料型別檢測

上面可以看到使用validate欄位去驗證欄位的值是否符合標準,這樣就可以在入庫之前就能知道資料是否符合規則。否則貿然將陌生的資料存入資料庫就好像將陌生人帶到家裡一樣,是否安全全靠緣分啊。

Sequelize內建支援的驗證還是非常的多的,如果這些都不滿意,還可以自己定義一個。

validate: {
    is: ["^[a-z]+$",'i'],     // 全匹配字母
    is: /^[a-z]+$/i,          // 全匹配字母,用規則表示式寫法
    not: ["[a-z]",'i'],       // 不能包含字母
    isEmail: true,            // 檢查郵件格式
    isUrl: true,              // 是否是合法網址
    isIP: true,               // 是否是合法IP地址
    isIPv4: true,             // 是否是合法IPv4地址
    isIPv6: true,             // 是否是合法IPv6地址
    isAlpha: true,            // 是否是字母
    isAlphanumeric: true,     // 是否是數字和字母
    isNumeric: true,          // 只允許數字
    isInt: true,              // 只允許整數
    isFloat: true,            // 是否是浮點數
    isDecimal: true,          // 是否是十進位制書
    isLowercase: true,        // 是否是小寫
    isUppercase: true,        // 是否大寫
    notNull: true,            // 不允許為null
    isNull: true,             // 是否是null
    notEmpty: true,           // 不允許為空
    equals: 'specific value', // 等於某些值
    contains: 'foo',          // 包含某些字元
    notIn: [['foo', 'bar']],  // 不在列表中
    isIn: [['foo', 'bar']],   // 在列表中
    notContains: 'bar',       // 不包含
    len: [2,10],              // 長度範圍
    isUUID: 4,                // 是否是合法 uuids
    isDate: true,             // 是否是有效日期
    isAfter: "2011-11-05",    // 是否晚於某個日期
    isBefore: "2011-11-05",   // 是否早於某個日期
    max: 23,                  // 最大值
    min: 23,                  // 最小值
    isArray: true,            // 是否是陣列
    isCreditCard: true,       // 是否是有效信用卡號
    // 自定義規則
    isEven: function(value) {
    if(parseInt(value) % 2 != 0) {
        throw new Error('請輸入偶數!')
    }
}
複製程式碼

API略講

Sequelize的API基本覆蓋了常用的使用方式,其中單表查詢常用的有一下幾種。複雜的可以參考更多的API。

查詢多條 findAll(opts) 或者 all(opts)

查詢用的引數普遍通用,只有部分API的有特殊引數。這裡展示一次常用引數,下面就略過了。

let list = await model.findAll({
	where:{
		id:{$gt:10},//id大於10的
		name:"test"  //name等於test
	},
	order:[
		"id",   //根據id排序
		["id","desc"]//根據id倒序
	],
	limit:10,//返回個數
	offset:20,//起始位置,跳過數量
	attributes:["attr1","attr2"], //返回的欄位
});
//select attr1,attr2 from model where ......
複製程式碼

通過id查詢 findById(id,opts)

這裡預設資料的主鍵是id,查詢的時候直接通過id查詢資料。這裡推薦在新建資料庫的時候可以新增id作為唯一主鍵。

let model = await model.findById(12);
//select a,b,c from model where id=12;
複製程式碼

查詢一條記錄 findOne(opts)

根據條件查詢記錄,這裡的條件一定要填寫,不然就是返回第一條資料了。

let model = await model.findOne({
	where:{id:12}
});
//select a,b,c from model where id=12;
複製程式碼

分頁查詢 findAndCount(opts) 或者 findAndCountAll

分頁查詢恐怕是另外一個常用方法了。任何一個列表都有需要分頁的時候。

這個方法會同時執行2跳語句。

let data = await model.findAndCount({
	limit:10,//每頁10條
	offset:0*10,//第x頁*每頁個數
	where:{}
});
let list = data.rows;
let count = data.count;
//select count(*) from model where ...;
//select a,b,c from model where .... limit 0,10;
複製程式碼

新增新資料 create(model,opts)

新增就非常的自在了。簡單的只需要傳入model物件即可。這裡要保證model物件的屬性和欄位名要一致。如果不一致就會出錯。也可以傳入配置引數來增加條件等。

let model= {
	name:"test",
	token:"adwadfv2324"
}
 await model.create(model);
//insert into model (name,token) values("test","adwadfv2324");
複製程式碼

查詢,不存在就返回預設物件 findOrInitialize(opts)

opts.default 預設值物件

這個方法首先會查詢資料庫,如果沒有結果就會返回引數中的default物件。這個比較適合返回預設物件之類的場景。

查詢,不存在就新建一個 findOrCreate(opts)或者findCreateFind

這個方法用到的情況也比較多。通常用於自動建立不存在的資料。直接就返回了預設值。

有則更新,無則新增 upsert(model,opts) 或者 insertOrUpdate(model,opts)

根據主鍵或者唯一約束鍵匹配

常用於編輯的時候新增或者更新統一操作。

更新記錄 update(model,opts)

就是最常用的更新方法,可以傳入要更新的model物件,同時用配置引數有條件的區別要更新的物件。

刪除記錄 destroy(opts)

刪除有2種情況,一種是物理刪除。刪除就從表中不存在了。另外一種就是設定paranoid,這個是虛擬刪除,預設一個欄位表示資料是否刪除,查詢的時候去掉這個條件即可查詢到刪除的資料。

恢復記錄 restore(opts)

恢復多個例項,當啟用paranoid時就可以使用這個方法將曾今刪除的資料恢復了。

其他常用API

  1. 指定欄位查詢最大值 max("id",opts)
  2. 指定欄位查詢最小值 min("id",opts)
  3. 求和 sum("id",opts)
  4. 批量新增 bulkCreate([model],opts)
  5. 查表結構的資訊 describe()
  6. 遞增 increment("id",{by:1})
  7. 遞減 decrement("id",{by:1})
  8. 統計查詢個數 count(opts)

事務

Sequelize中的事務比較簡單。但是如果有多個事務的話寫出來的程式碼會非常的難看。這也算是Sequelize優化的比較差的地方了。

需要記得transaction引數要一致傳遞就可以了。其他就是一個正常的Promise呼叫。

//呼叫Sequelize初始化之後的sequelize物件
return sequelize.transaction(function (t) {
	//返回最終的Promise
  return User.create({
    firstName: 'Abraham',
    lastName: 'Lincoln'
  }, {transaction: t}).then(function (user) {
    return user.setShooter({
      firstName: 'John',
      lastName: 'Boothe'
    }, {transaction: t});
  });
}).then(function (result) {
  //主動呼叫commit提交結果
  return t.commit();
}).catch(function (err) {
  //主動回滾操作
  return t.rollback();
});
複製程式碼

多表聯查

外來鍵可能算是Sequelize中的一個難點了。這裡涉及的東西稍微多一點,我們來慢慢捋一遍。

外來鍵知識點

外來鍵的定製作用----三種約束模式:

  1. district:嚴格模式(預設), 父表不能刪除或更新一個被子表引用的記錄。
  2. cascade:級聯模式, 父表操作後,子表關聯的資料也跟著一起操作。也是Sequelize的預設模式。
  3. set null:置空模式,前提外來鍵欄位允許為NLL, 父表操作後,子表對應的欄位被置空。

使用外來鍵的前提

在Sequelize中使用外來鍵需要提前檢查一下下面的這些選項,裡面有一條出錯就會導致設定失敗。

  1. 表儲存引擎必須是innodb,否則建立的外來鍵無約束效果。
  2. 外來鍵的列型別必須與父表的主鍵型別完全一致。
  3. 外來鍵的名字不能重複。
  4. 已經存在資料的欄位被設為外來鍵時,必須保證欄位中的資料與父表的主鍵資料對應起來。

使用示例---預設

預設情況下,主鍵使用的是主表的id欄位,外來鍵是使用的按照table+欄位的方式建立的外來鍵。一般情況下需要手動指定。

//主表指定關係
 test1.hasMany(test2, {
     foreignKey: "pid",//外來鍵名稱
 });
 //子表指定關係
 test2.belongsTo(test1, {
     foreignKey: "pid",//外來鍵名稱
 });
複製程式碼

預設就會在子表中新增一條外來鍵記錄,指向的就是主表的id。一般情況下這樣就能夠滿足正常的使用了。比如一個主表記錄商品資訊,一個子表記錄多個評論訊息。

使用示例---自定義

如果主表使用的主鍵id並不能滿足正常的使用,還可以指定某一個固定的欄位作為主表中的約束關係。

tips:主表中如果不是使用id作為主要關係,自定義的欄位必須新增索引等條件,作為依賴中的關係。

 test1.hasMany(test2, {
     foreignKey: "pid",//外來鍵欄位名
     sourceKey: "pid",//主鍵欄位名
 });
 test2.belongsTo(test1, {
     foreignKey: "pid",//關聯名
     targetKey:"pid"//自定義外來鍵欄位
 });
 //等待主鍵建立成功再建立子表的外來鍵關係
 setTimeout(() => {
    test2.sync({
        force: true
    });
}, 2500);
複製程式碼

使用示例---偽關係

實際使用的時候我還是傾向於這種關係。即表中關係已定的情況下僅僅指定外來鍵關係。同步的時候僅僅同步表內容,不同步這個外來鍵關係。

真正的建立可以使用手動建表的時候新增。或者也可以在自動建表結束後非同步再執行一次外來鍵關係的新增。

 test1.hasMany(test2, {
     foreignKey: "pid",
     sourceKey: "pid",
     constraints: false //不同步建立外來鍵關係
 });
 test2.belongsTo(test1, {
     foreignKey: "pid",
     targetKey:"pid",
     constraints: false //不同步建立外來鍵關係
 });
複製程式碼

示例

實際的操作部分大家可以看github中的test.js。github地址

單表操作

Sequelize在查詢結果返回之後會返回一個它自定義的物件。這個物件是支援繼續操作的,其中具體的值存放在datavalues中。不過可以放心的是在轉化為字串的時候是不會帶有任何Sequelize的屬性的。

//根據條件查詢一條資料
let model = await test1.findOne({
	where:{
		id:5,
		name:"test"
	}
});
//修改其中的name欄位的值
model.name="更新";
//儲存,會自動update資料庫中的值
model.save();
複製程式碼

聯查

正常的使用過程中很少會說只需要查詢一個表就能結果問題的。這裡再說一下2個表查詢的時候是怎麼使用的。

這裡的查詢預設已經做好了外來鍵的的關係。不過在使用的時候不做也是可以的,就是在查詢的時候效能稍微不好而已。

//查詢主表list的資料
//一條list中的資料對應多條item中的資料
 let data = await models.List.findAll({
	 where:{id:5},//條件,這裡jiashe只需查詢一條
     include: [{
         model: models.Item,
         as:"items",//返回的物件修改成一個固定的名稱
     }]
 });
 let list1=data[0];//返回的第一條資料就是要查詢的資料
 let list2=list1.items;//返回子表資料,items是自定義的名稱
複製程式碼

總結

上面的介紹已經解決了大多數情況下的查詢等操作。而且我也相信,真的遇到了瓶頸,解決方案很可能也並不是在Sequelize方面,或者說主要不是Sequelize的問題。比如大資料量的時候分表操作,就涉及到了更多的知識點。

nodejs在做後端方面還處於發展階段。希望有更多的前端能夠接觸並瞭解它。不僅僅在開發過程中對自己是一個增強,在長期的職業規劃中也是一個很好的增強自己的武器。

相關文章