Mongoose教程翻譯

原文地址

本文簡單的介紹了數據庫,以及如何在 Node/Express 中應用他們。之后展示如何使用Mongoose為一個圖書網站提供數據訪問。介紹了Mongoose的模式和模型的聲明,主要屬性類型,基礎校驗器。也展示了訪問數據模型的幾種主要方法。

概括


圖書管理員將使用一個 圖書管理網站 去記錄書籍和借書人的信息,同時讀者會使用 圖書管理網站 去瀏覽搜索圖書,去查看是不是某書全部借出,然后預約或直接借走該書。為了更有效率存儲信息,這里我們需要一個數據庫。

Express提供了多種數據庫的支持,你有多種方式去實現增刪查改操作。本文簡要的概述了一些可用的選項,并詳細說明一些查詢的特殊機制。

使用什么數據庫?

Express應用可以使用Node支持的各種數據庫(Express本身對數據庫并沒有特殊要求)。常用的選項包括PostgreSQL,MySQL,Redis,SQLite,MongoDB。

當選擇一個數據庫時,我們需要考慮的常常有 時間成本,學習曲線,性能,備份和回滾的易用性,成本以及其社區支持情況。雖然并沒有一個最好的數據庫,但是對于我們的 圖書管理網站 這樣的小型網站,任何流行的數據庫都是可以的。

如何更好的與數據庫交互?

與數據庫交互有兩種方法:

  1. 使用數據庫的自帶的查詢語言(比如SQL)
  2. 使用對象數據模型("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

天殺的UML圖

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提供來內置的校驗器,自定義校驗器,同步或者異步校驗器。他用來指定可用的范圍或者值,以及在校驗失敗時的錯誤信息。

內置的校驗器包括:

  • 所有的類型都包含required校驗器。這個校驗器是用來指定在保存時,屬性是否是必須的。
  • Numbersminmax校驗器。
  • Strings類型有:

下面的樣例略微修改自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()方法會查詢所有匹配的記錄,但是通常我們只需要其中的一條。下面的方法用以查詢一條記錄:

提示:也有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"
});

血條耗盡。。。。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容