本文簡單的介紹了數據庫,以及如何在 Node/Express 中應用他們。之后展示如何使用Mongoose為一個圖書網站提供數據訪問。介紹了Mongoose的模式和模型的聲明,主要屬性類型,基礎校驗器。也展示了訪問數據模型的幾種主要方法。
概括
圖書管理員將使用一個 圖書管理網站 去記錄書籍和借書人的信息,同時讀者會使用 圖書管理網站 去瀏覽搜索圖書,去查看是不是某書全部借出,然后預約或直接借走該書。為了更有效率存儲信息,這里我們需要一個數據庫。
Express提供了多種數據庫的支持,你有多種方式去實現增刪查改操作。本文簡要的概述了一些可用的選項,并詳細說明一些查詢的特殊機制。
使用什么數據庫?
Express應用可以使用Node支持的各種數據庫(Express本身對數據庫并沒有特殊要求)。常用的選項包括PostgreSQL,MySQL,Redis,SQLite,MongoDB。
當選擇一個數據庫時,我們需要考慮的常常有 時間成本,學習曲線,性能,備份和回滾的易用性,成本以及其社區支持情況。雖然并沒有一個最好的數據庫,但是對于我們的 圖書管理網站 這樣的小型網站,任何流行的數據庫都是可以的。
如何更好的與數據庫交互?
與數據庫交互有兩種方法:
- 使用數據庫的自帶的查詢語言(比如SQL)
- 使用對象數據模型("ODM")或對象關系模型 ("ORM")。一個ODM或ORM對象代表的就是一個映射到底層數據庫的數據對象比如說JSON對象。一些ORM對象是指定數據庫的,一些則不然。
使用SQL語言或者其他數據庫支持的語言可以獲得很好的性能。ODM則相對比較慢,因為需要代碼去轉換映射的對象和數據庫中的格式,所以他生成的查詢語句可能不夠高效(尤其是在ODM為了支持不同的數據庫后臺,這時必須對數據庫功能做出極大的妥協)。
使用ODM的優勢在于程序員可以一直關注與JavaScript 對象而不是數據庫語義,尤其是在你需要和不同的數據庫交互(可能是同一應用,或不同應用)。ODM也提供了清晰方式去校驗檢查數據。
使用ODM或ORM可以降低開發和維護成本,除非你非常擅長原生查詢語言,或對性能要求很高,否則你都應該優先考慮使用ODM或ORM。
使用什么ODM/ORM
在npm中有許多ODM、ORM。
在本文寫作時幾個熱門的框架
- Mongoose:Mongoose是一個用于異步環境的MongoDB的對象模型。
- Waterline:提取自基于Express的Sail框架的對象關系模型。他為眾多數據庫提供了統一的API接口,包括 Redis, mySQL, LDAP, MongoDB, 和 Postgres。
- Bookshelf:同時具備promise和傳統回調函數的接口,提供了對事務的支持,eager/nested-eager relation loading(不知道咋翻),集成多態,支持 一對一,一對多,多對多關系。支持PostgreSQL, MySQL, 和 SQLite3。
- Objection:盡可能的簡化的使用數據庫和SQL的全部功能(支持SQLite3, Postgres 和 MySQL)
- Sequelize:基于promise的ORM...
在選擇解決方案時一般應該考慮他們都提供哪些功能,以及他們社區的活躍度(下載,捐款,Bug報告,文檔質量)。在此文寫作時Mongoose當前最受歡迎的ORM,如果你在你的應用中使用MongoDB作為你的數據庫,那么他是一個合理的選擇。
在LocalLibrary(這篇文章中的項目名)使用MongoDB和Mongoose
在本文中我們使用Mongoose來訪問我們的圖書數據。Mongoose作為MongoDB的前端,MongoDB是一個開源的NoSQL,使用面向文檔的對象模型的數據庫。在MongoDB中 集合(collection)中的文檔(documents)類似于關系數據庫中的表(table)中的行(row)。
這對ODM和數據庫組合在Node的社區中是非常流行的,部分原因是因為文檔存儲和查詢起來非常類似于JSON,對于JS程序員這是非常熟悉的。
你不必為了使用Mongoose而去了解MongoDB,但是如果你已經了解MongoDB,可以更容易的使用和理解Mongoose
教程后續部分將講解如何為LocalLibrary定義以及使用Mongoose模板和模型。
LocalLibrary的model設計
當你開始進行model編碼的時候,花一些時間考慮你應該需要存儲什么數據,以及不同對象之間的關系。
我們知道我們需要存儲有關書籍的信息(書名,概要,作者,類型,書號),而且一種書我們可能有多本(擁有唯一的ID,可用狀態),我們可能也需要存儲除了姓名之外其他的作者信息,而且可能會有多個作者名是相同或相似的。我們還想能夠通過 書名,作者,書的類別進行排序。
當你設計你的Model時為不同的Object(擁有一組相關的信息的對象)設置不同的Model是必要的。在當前的實例中明顯的對象有 書籍,書籍實體,作者。
你可能也想為一個下拉列表選項新建一個Model。相對于硬編碼,當下拉列表不確定或經常更改,這種方式更加推薦。在本例中書籍類型(科幻小說,法國詩歌)明顯就是屬于這種類型。
一旦我們確定了Model和相應的屬性,我們就需要思考他們之間的關系。
考慮到這一點,下面的UML關系圖展示了當前我們定義的Model。根據上文的討論,我們將為書籍(包含書籍的一般信息),書籍實體(包含這本書在系統中的狀態),作者 創建Model。同時我們也決定為書籍的類別創建一個Model,這樣書的類別就可以動態修改。書籍實體的狀態并不常變化,我們不會為他單獨創建Model。在每個方塊中我們定義了Model的名字,以及屬性名和類型,還有方法名和方法返回值的類型。
該圖也展示了Model直接的關系,和他們數量的對應關系(最大和最?。?。比如在Book和Genre之間的連線,在靠近Book的數字表示一本書有0或者多個Genre,而另一端的數字表示每個Genre有0或者多個Book
Mongoose入門
這部分概括了如何用Mongoose連接MongoDB,如何定義Schema(這個之前被我翻成模板,不知道對不對,后面索性不返了) 和Model,如何進行簡單的查詢。
安裝Mongoose和MongoDB
Mongoose和其他的依賴一樣被安裝,使用以下命令為你的項目安裝Mongoose。
npm install mongoose --save
安裝Mongoose會自動加入他的依賴,例如MongoDB的驅動,但并沒有安裝MongoDB數據庫本身。如果你想要安裝MongoDB你可以從這里下載多個平臺的安裝包。你也可以使用云端的MongoDB實例。
提示:在本文中我們將使用mLab的云端作為數據庫。這樣非常適合開發,對于這個教程這樣做也是有意義的,因為他使安裝過程不依賴讀者的操作系統。
連接到MongoDB
Mongoose會向MongoDB請求連接。你能夠使用require()引入Mongoose,并使用mongoose.connect()連接到本地數據庫,如下所示:
//Import the mongoose module
var mongoose = require('mongoose');
//Set up default mongoose connection
var mongoDB = 'mongodb://127.0.0.1/my_database';
mongoose.connect(mongoDB);
//Get the default connection
var db = mongoose.connection;
//Bind connection to error event (to get notification of connection errors)
db.on('error', console.error.bind(console, 'MongoDB connection error:'));
用mongoose.connection你可以獲得Mongoose的默認Connection 對象。一旦連接完成,open 事件將從Connection 實例中發射。
提示:如果你需要額外的Connection,你可以使用mongoose.createConnection(),他接受一個和connect()相同格式的數據庫URI(包含host,數據庫名,端口,選項),返回一個Connection 實例。
定義并創建Model
Model是使用Schema 接口來定義。Schema 用來定義存儲在document 中的屬性,并且賦予他們校驗的規則,和默認值。另外你可以定義靜態 或 實體 的helper方法,使你的數據類型更加易于使用。你也可以定義像其他屬性一樣使用的虛擬屬性,這些屬性并不會被保存到數據庫中(這些會在后文中講解)。
Schema使用mongoose.model()去“編譯”入Model。一旦擁有一個model ,你可以使用他用來創建,查詢,刪除指定的對象。
Model對應的是MongoDB中documents的collection,documents中包含在Schema中的定義的屬性和屬性對應的類型。
定義Schema
下面的代碼展示了如何定義一個簡單的Schema。首先引入mongoose,然后使用Schema的構造方法新建一個Schema的實例,在構造函數的參數對象中定義屬性。
//Require Mongoose
var mongoose = require('mongoose');
//Define a schema
var Schema = mongoose.Schema;
var SomeModelSchema = new Schema({
a_string: String,
a_date: Date
});
在上面的例子中,我們只有兩個屬性,一個字符串,一個時間。在本文的下一段我們會展示其他屬性類型,和校驗器和其他方法。
創建一個Model
Model是使用mongoose.model()創建自Schema。
// Define schema
var Schema = mongoose.Schema;
var SomeModelSchema = new Schema({
a_string: String,
a_date: Date
});
// Compile model from schema
var SomeModel = mongoose.model('SomeModel', SomeModelSchema );
第一個參數是MongoDB中的集合的名,mongoose將為上面的Model創建一個名為SomeModel的集合。第二個參數是你想要用來創建Model的Schema 。
一旦你創建了Model,你可以使用他來進行增刪查改,既可以查詢全部記錄,也可以查詢特定的子集。當我們創建我們的視圖時,我們會在“使用Model”段講解如何做。
Schema 中屬性的類型
Schema 可以有任意數量的屬性,每一個屬性都代表了在MongoDB中的字段。下面的例子展示了常用的屬性類型是如何被定義的。
var schema = new Schema(
{
name: String,
binary: Buffer,
living: Boolean,
updated: { type: Date, default: Date.now },
age: { type: Number, min: 18, max: 65, required: true },
mixed: Schema.Types.Mixed,
_someId: Schema.Types.ObjectId,
array: [],
ofString: [String], // You can also have an array of each of the other types too.
nested: { stuff: { type: String, lowercase: true, trim: true } }
})
大多數屬性類型的意義是顯而易見的,除了以下幾項:
- ObjectId:代表在數據庫中的一個對象實體,比如,書本對象可以使用他來代表他的作者。實際上他包含的是對象的唯一id(_id)。我們可以在需要的時候使用populate()方法獲取某些信息。
- Mixed:任意schema類型。
- []:數組對象。你可以對該對象執行JavaScript數組操作(push, pop, unshift等)。上面的實例展示了,沒有指定數組對象類型的數組,和指定為String的數組。你可以指定任意類型的數組。
這段代碼也展示來定義屬性的兩種方法:
- 屬性名和屬性類型作為鍵值對。
- 屬性名后緊跟一個對象來定義屬性類型,以及屬性的其他選項。選項包含以下這些:
- 默認值
- 內置的校驗器(min/max),或者定制的校驗函數。
- 屬性是否是必須的。
- 屬性是否會自動大寫,小寫或者去除空格(e.g. { type: String, lowercase: true, trim: true })。
更多有關選項的信息,請看SchemaTypes的文檔。
校驗器
Mongoose提供來內置的校驗器,自定義校驗器,同步或者異步校驗器。他用來指定可用的范圍或者值,以及在校驗失敗時的錯誤信息。
內置的校驗器包括:
下面的樣例略微修改自Mongoose的文檔,展示來如何指定校驗器的類型和錯誤信息。
var breakfastSchema = new Schema({
eggs: {
type: Number,
min: [6, 'Too few eggs'],
max: 12
required: [true, 'Why no bacon?']
},
drink: {
type: String,
enum: ['Coffee', 'Tea', 'Water',]
}
});
詳細的校驗器說明,請看Mongoose的文檔Validation
虛擬屬性
虛擬屬性是你可以get和set的對象屬性,但是他們不會被保存到MongoDB中。get方法常常被用來格式化或者合并屬性,set方法常用來分解單個屬性并把他們保存在數據庫中的多個屬性中。在本例中用first name和last name屬性去構造一個全名,相對于每次在使用時來構造一個全名更加清晰和簡單。
備注:我們將使用虛擬屬性為每條記錄的_id屬性和地址定義一個唯一URL。
更多信息請看Virtuals
方法和查詢助手
schema可以有實體方法,靜態方法和查詢助手。實體方法和靜態方法是類似的,他們之間明顯的不同是,實例方法是關聯到實際對象的,能夠訪問當前對象。查詢助手允許你擴展mongoose的查詢構造器API(比如,你可以添加“byName”查詢方法去擴展find(), findOne() 和 findById())。
Model的使用
一旦你創建創建了schema,你就可以使用他來創建Model。Model代表了數據庫中Document的Collection,而一個Model的實體代表了一個你可以存取的單一對象。
下面我們提供一個概述,詳情請看Models 。
創建和更改document
你可以通過創建一個Model實體并調用save()方法去保存一條記錄。下面的例子假設SomeModel是通過schema創建的某個對象(只有一個“name”屬性)。
// Create an instance of model SomeModel
var awesome_instance = new SomeModel({ name: 'awesome' });
// Save the new model instance, passing a callback
awesome_instance.save(function (err) {
if (err) return handleError(err);
// saved!
});
注意記錄的創建(以及更新,刪除,查詢)是異步操作,你需要傳遞一個回調函數,當操作完成時會執行。我們遵從錯誤優先的慣例,所以回調函數的第一個參數為錯誤信息,如果有的話。如果操作會返回結果,他將被作為第二個參數。
你也可以使用create()方法,在你定義對象的同時保存他?;卣{函數將返回錯誤信息作為第一個參數,創建的實體作為第二個參數。
SomeModel.create({ name: 'also_awesome' }, function (err, awesome_instance) {
if (err) return handleError(err);
// saved!
});
每一個Model都有一個相關的連接對象(當你使用model()方法時,會使用默認方法),你可以創建一個新的連接,并調用他的model()方法,用以在不同的數據庫中創建記錄。
你可以使用點語法去訪問對象屬性,更改屬性值。你必須使用save()或update()將變更保存到數據庫中。
// Access model field values using dot notation
console.log(awesome_instance.name); //should log 'also_awesome'
// Change record by modifying the fields, then calling save().
awesome_instance.name="New cool name";
awesome_instance.save(function (err) {
if (err) return handleError(err); // saved!
});
搜索數據
你可以通過查詢方法去檢索數據記錄,并用JSON對象來指定查詢條件。下面的代碼展示了如何查詢所有參加網球運動的運動員,并只返回姓名和年齡。這里我們只匹配了運動這一個屬性,但是你們可以指定更多的檢索條件,如一個正則表達式,或者不要任何條件,返回所有數據。
var Athlete = mongoose.model('Athlete', yourSchema);
// find all athletes who play tennis, selecting the 'name' and 'age' fields
Athlete.find({ 'sport': 'Tennis' }, 'name age', function (err, athletes) {
if (err) return handleError(err);
// 'athletes' contains the list of athletes that match the criteria.
})
如果你向上面一樣指定了回調方法,查詢會馬上執行,而回調方法會在查詢完成后執行。
在mongoose中所有回調函數都采用了callback(error, result)的形式。如果在查詢時發生錯誤,error中將包含錯誤信息,而result將返回null。如果查詢成功error是null,而result中包含查詢的結果。
如果你沒有傳遞回調方法,程序將返回一個Query對象。你可以使用這個query對象去組建你的查詢,之后調用exec()方法執行他,并傳入回調方法。
// find all athletes that play tennis
var query = Athlete.find({ 'sport': 'Tennis' });
// selecting the 'name' and 'age' fields
query.select('name age');
// limit our results to 5 items
query.limit(5);
// sort by age
query.sort({ age: -1 });
// execute the query at a later time
query.exec(function (err, athletes) {
if (err) return handleError(err);
// athletes contains an ordered list of 5 athletes who play Tennis
})
上面的代碼中,我們在find中指定了查詢條件。我們也可以使用where()方法,他能夠使用(.)點語法將所有查詢條件連接起來,而不用分別指定。下面的代碼等同于上面的代碼,但是我們添加了一個age查詢條件。
Athlete.
find().
where('sport').equals('Tennis').
where('age').gt(17).lt(50). //Additional where query
limit(5).
sort({ age: -1 }).
select('name age').
exec(callback); // where callback is the name of our callback function.
find()方法會查詢所有匹配的記錄,但是通常我們只需要其中的一條。下面的方法用以查詢一條記錄:
- findById():通過id查詢
- findOne():依照一定的條件查詢一條記錄。
- findByIdAndRemove(), findByIdAndUpdate(), findOneAndRemove(), findOneAndUpdate(): (這個自己看名字也知道了,我就不翻了)
提示:也有count()方法,獲取指定條件的記錄數。常常用于,你只想要知道數目而不是實際的記錄時。
查詢中你還可以作很多,詳情請看:Queries。
處理相關的對象 -----熱門
你可以使用ObjectId 屬性類型創建一個索引連接兩個對象,或者使用ObjectId 的數組去連接多個對象。這個屬性存儲著model的id。如果你需要關聯對象的實際內容,你可以使用populate()方法去查詢并替換id為真實數據。
例如,一下的schema定義了作者和故事。每個作者有多個故事,我們將使用ObjectId 數組來表示他們。一個故事只有一個作者。"ref"屬性(高亮加粗顯示的,makedown)告訴schema 連接哪個model。
var mongoose = require('mongoose')
, Schema = mongoose.Schema
var authorSchema = Schema({
name : String,
stories : [{ type: Schema.Types.ObjectId, ref: 'Story' }]
});
var storySchema = Schema({
author : { type: Schema.Types.ObjectId, ref: 'Author' },
title : String,
});
var Story = mongoose.model('Story', storySchema);
var Author = mongoose.model('Author', authorSchema);
我們可以使用_id值去保存關聯對象的索引。下面我們創建一個author,之后是一個book對象,并關聯author對象到author屬性。
var bob = new Author({ name: 'Bob Smith' });
bob.save(function (err) {
if (err) return handleError(err);
//Bob now exists, so lets create a story
var story = new Story({
title: "Bob goes sledding",
author: bob._id // assign the _id from the our author Bob. This ID is created by default!
});
story.save(function (err) {
if (err) return handleError(err);
// Bob now has his story
});
});
我們的story對象依靠id獲得了author的索引。為了獲得詳細的author信息我們使用populate()方法。如下:
Story
.findOne({ title: 'Bob goes sledding' })
.populate('author') //This populates the author id with actual author information!
.exec(function (err, story) {
if (err) return handleError(err);
console.log('The author is %s', story.author.name);
// prints "The author is Bob Smith"
});