談?wù)凪ongoDB的三層操作
NOSQL近來勢頭不錯,MongoDB更是其中的嬌嬌者,自己學(xué)NoSQL的時候也是參考了大量的資料,最終決定要從MongoDB入手的,最重要的原因有兩點:1自己是簡單的愛好者,一切問題我都在想是否有簡單的方法解決,寧可停下來去思考大量時間,也不愿用笨方法馬上去做,而MongoDB的操作大都很簡單,2自己是JS的愛好者,沒事就喜歡拿一本js的本從頭到尾看一邊,也不管記住多少,也不管用不用得到,就是喜歡,MongoDB以BSON格式存儲,所以操作也起來也算得心應(yīng)手!現(xiàn)在做一個項目正是用MongoDB做為數(shù)據(jù)庫的,一開始沒有DAL,BLL直接訪問數(shù)據(jù)庫,然后就到UI了,而且BLL是全靜態(tài)的(我喜歡靜態(tài)方法的調(diào)用簡單,但狠靜態(tài)類的不能繼承?。?,當(dāng)時考慮的是用MongoDB的驅(qū)動去操作太直白了!感覺沒必要再寫個DAL!,后來知道我想法還是很天真的,哈哈!下面就說現(xiàn)在的操作方式吧~
一、Module層
[Serializable]
public sealed class user
{
public ObjectId id;
public string n;
public int age;
public Birthday birth;
public sealed class Birthday
{
public int y;
public int m;
public int d;
}
}
咋一看,有幾個地方不規(guī)范,1類名首字母和公開字段沒有大寫,2公開的字段,而沒有用屬性,3字段命名沒表達它的意思。現(xiàn)在逐個解釋一下,類名和字段沒大寫首字母主要是數(shù)據(jù)庫里的命名是遵循js的,用js表示時大家一般會這樣寫:
var user={id:ObjectId("123456"),n:"loogn",age:23,birth:{y:1989,m:7,d:7}}
然而,可能有人會說可以用MongoDB.Bson.Serialization.Attributes.BsonElement這樣一個Attribute關(guān)聯(lián)呢!不過我不會那樣做,原因就是太麻煩了!這里可能還是有疑問,留到第3個問題說。
公開字段而沒有用屬性也是不合常理的,學(xué)校老師都交過,不管你能不能理解,反正就是要私有化字段,想公開請用屬性,哈哈!不過我想了很久還是不走尋常路了,目前為止用字段沒有出現(xiàn)過缺陷問題,我不保證以后不會,但我感覺幾率十分小,就算真的有什么問題必需要用屬性,后面加上{get;set;}也不麻煩吧!屬性畢竟還是方法,用屬性有多余的方法調(diào)用開銷,而且實體類本來就是不尋常的類,一般只表示對象狀態(tài)(用字段),屬性里如果有邏輯(就像老師常說的年齡不能小于0且不能大于150等等!),你會放到這里做嗎?顯然你都是放在BLL里做!字段命名太短了沒有表達它的意思,其實這個可以和***個一起來說,MongoDB是無模式的,同一個合集可以保多個不規(guī)則的文檔,如user集合:
{id:1,n:"user1",desc:"我的描述"}
{id:2,n:"user2"}
所以數(shù)據(jù)庫必須保存文檔的每一個元素的name(即id,name,desc,id,name),所以元素name越短越節(jié)省空間,本來是用name更能表達的,這里用了n,其實只要把常用的約定一下,絕大部分人都是可以接受的。
在user里還有個內(nèi)嵌類Birthday,而這個類大寫了首字母,我是這樣考慮的,內(nèi)嵌類名全部按C#命名規(guī)范,因為容器類有一個該內(nèi)嵌類類型的字段,這里是birth,但如果找不到合適的縮寫怎么辦呢,直接小寫內(nèi)嵌類名就可以了,如內(nèi)嵌城市類City,字段名為city就不會重復(fù)了。
二、DAL層
在這一層要寫一個基類,完成這個基類后,其他的各各DAL類都是浮云了~,在寫基類之前有一個MongoHelper,MongoHelper很簡單,直接給出代碼且不寫解釋:
MongoServer
完了后就可以寫B(tài)aseDAL了,如果沒有泛型,DAL的工作還真是索然無味,但現(xiàn)在用泛型DAL的工作有趣多了,先承上代碼:
/// <summary>
/// 數(shù)據(jù)訪問層基類
/// </summary>
/// <typeparam name="T">文檔實體類</typeparam>
public abstract class BaseDAL<TDocument>
{
protected internal string CollectionName { set; get; }
/// <summary>
/// 設(shè)置集合名
/// </summary>
protected abstract string SetCollectionName();
private MongoCollection<TDocument> m_collection;
/// <summary>
/// 根據(jù)CollectionName得到MongoCollection對象
/// </summary>
protected internal MongoCollection<TDocument> Collection
{
get
{
if (m_collection == null)
{
CollectionName = SetCollectionName();
m_collection = MongoHelper.GetDatabase().GetCollection<TDocument>(CollectionName);
}
return m_collection;
}
}
/// <summary>
/// 根據(jù)query條件得到一個文檔對象
/// </summary>
/// <param name="query">查詢條件</param>
/// <param name="preprocess">預(yù)處理方法</param>
/// <returns></returns>
public TDocument FindOne(IMongoQuery query, PreprocessHandler<TDocument> preprocess)
{
var document = Collection.FindOne(query);
if (preprocess != null)
{
preprocess(ref document);
}
return document;
}
/// <summary>
/// 把MongoCursor轉(zhuǎn)換成IList類型
/// </summary>
/// <param name="cursor">文檔游標(biāo)</param>
/// <param name="preprocess">預(yù)處理方法</param>
/// <returns></returns>
protected internal IList<TDocument> CursorToList(MongoCursor<TDocument> cursor, PreprocessHandler<TDocument> preprocess)
{
IList<TDocument> list = new List<TDocument>(30);
bool isPreprocess = preprocess != null;
foreach (TDocument document in cursor)
{
var doc = document;
if (isPreprocess)
preprocess(ref doc);
list.Add(doc);
}
return list;
}
/// <summary>
/// 根據(jù)query查詢集合
/// </summary>
/// <param name="query">條件</param>
/// <param name="preprocess">預(yù)處理方法</param>
/// <returns></returns>
public IList<TDocument> Find(IMongoQuery query, MongoCursorSettings cursorSettings, PreprocessHandler<TDocument> preprocess)
{
var cursor = Collection.Find(query);
if (cursorSettings != null)
{
cursorSettings.Set(cursor);
}
var list = CursorToList(cursor, preprocess);
return list;
}
}
最上面的代碼就是設(shè)置操作哪個集合,這里有一點感覺不爽,為什么屬性的get和set不能分別為抽象的呢?!雖然可以把整個屬性標(biāo)記為abstract,但在實現(xiàn)類中也要寫get和set的實現(xiàn)(set可以是空代碼塊),所以這里回歸原本用了一個SetCollectionName的抽象方法讓子類去設(shè)置自己對應(yīng)的集合名。
當(dāng)你得到MongoCollection對象,特別是MongoCollection<TDocument>這樣的強類型對象,BaseDAL剩下的工作也成浮云了!(都是對驅(qū)動方法的封裝和個性化處理),如FindOne方法,用到一個委托:
public delegate void PreprocessHandler<T>(ref T document);
有很多這樣的情況,得到一個實體后總是要先處理一下才可被UI方便的使用,如用戶頭像為空時,給個默認的,PreprocessHandler就是給BLL處理留個方便的接口啦!
這里選擇委托而不是其他的元素使程序更靈活(有匿名委托嘛,I like it!),大家注意到了吧,實體類是按引用傳遞的,其實這里有個坑,我跳進去了,但又爬上來了!然后這里立了個牌:"此處有坑,請繞道而行",以免匆忙趕路的你也栽個跟頭兒。
有這樣一個情況,如果你把一個null實體對象傳入處理方法,又在處理方法里判斷如果是null就實體化,這樣是得不到預(yù)期效果的,此null非彼null呀,實體化后方法里的型參是指向新對象了,但傳過來的實參還是指向null呢,這不是我們想要的,用ref便可以解決了,也許你會說可以把實體對象返回呀,是的,但個人不喜歡那種寫法,那樣處理方法***還要寫reurn代碼,調(diào)用方法可能還得寫代碼接收,麻煩!
FindOne例子完了,但FindOne遠遠沒完,你可以做各種你喜歡的重載。
再看Find得到多個文檔的方法,這里我選擇IList<實體>做為返回值,在BLL決不去操作MongoCursor,但有這樣一個問題,設(shè)置Fields、Limit、排序等都是在MongoCursor上操作的呀,而且這些操作很可能都是從UI傳過來的,所以這里用了一個MongoCursorSettings類封裝了這些設(shè)置:
MongoCursorSettings
代碼不用解釋,你可以擴展此類做更多設(shè)置而不用修改Find方法。
CursorToList方法也很簡單,其實把PreprocessHandler寫在DAL層的原因就是這個方法啦,心細的你肯定發(fā)現(xiàn)了把PreprocessHandler寫在BLL更合理,但那樣文檔集合就要多遍歷一遍,MongoCursor到IList一遍,PreprocessHandler處理IList又一遍!唉,程序員容易嘛~~~
當(dāng)然,你也可以對Find做各種你喜歡的重載,更要寫其他方面(Insert,Update,Remove...)的方法對BaseDAL類進行完善。
***讓親看一下User浮云(其他浮云也是這個樣):
public class User:BaseDAL<user>
{
protected override string SetCollectionName()
{
return "user";
}
}
三、BLL層
有泛型就是意思,看BaseBLL:
/// <summary>
/// 業(yè)務(wù)邏輯層基類
/// </summary>
/// <typeparam name="TDAL">數(shù)據(jù)訪問類型</typeparam>
/// <typeparam name="TDocument">文檔模型類型</typeparam>
public abstract class BaseBLL<TDAL, TDocument> where TDAL : DAL.BaseDAL<TDocument>,new()
{
protected TDAL dal = new TDAL();
public TDocument FindOne(IMongoQuery query, PreprocessHandler<TDocument> preprocess)
{
return dal.FindOne(query,preprocess);
}
}
基本上是對DAL的一個調(diào)用,無他!直接到它的子類,因為是邏輯層,比浮云多一點,可以算是個神馬吧:
public sealed class User : BLL.BaseBLL<DAL.User, user>
{
public user FindOneByName(string name)
{
var doc = base.FindOne(Query.EQ("u", name), P1);
return doc;
}
/// <summary>
/// 保證不為null
/// </summary>
/// <param name="doc"></param>
private void P1(ref user doc)
{
if (doc == null)
{
doc = new user();
}
P2(ref doc);
}
/// <summary>
/// 也許可以處理嬰兒,哈哈
/// </summary>
/// <param name="doc"></param>
private void P2(ref user doc)
{
if (doc != null)
{
doc.age = 0;
}
}
}
代碼也是很簡單,其實這里有一個想法,很多實體類總是只有一種處理方法,可以在BaseBLL里寫一個PreprocessHandler委托簽名的虛方法做為默認處理方法, 在BaseBLL里就調(diào)用該方法,子類需要就可重寫它,這樣又簡單了,為了方面查看,兩個類的代碼寫在一塊了:
/// <summary>
/// BaseBLL的默認處理方法
/// </summary>
/// <param name="doc"></param>
protected virtual void Preprocess(ref TDocument doc)
{
return;
}
/// <summary>
/// LG.BLL.User重寫基類方法
/// </summary>
/// <param name="doc"></param>
protected override void Preprocess(ref user doc)
{
if (doc == null)
doc = new user();
if (doc.birth == null)
doc.birth = new user.Birthday();
}
到此,BLL事例也完了,當(dāng)然,還要做更多的工作去完善。
四、UI層
這里其實沒啥說的,為了文章完整性,簡單提一下。
一般情況下UI是不會引用DAL的,但因為BaseBLL用了泛型參數(shù),而泛型類型在DAL里,所以UI也要引用DAL,但永遠不要用DAL。
還有一點,就是邏輯層實例化的問題,比如在同一次Http請求中,很可能不小心或者避免不了new了LG.BLL.User多次,這樣做只是白白浪費了內(nèi)存,增加GC壓力,沒一點好處,所以,再回到BLL層,添加這樣一個類:
/// <summary>
/// 為了在一次請求中同類型邏輯對象只實例化一次,
/// 只能在http請求上下文中使用
/// </summary>
public static class B
{
public static T Entity<T>() where T : class, new()
{
string key = typeof(T).Name;
if (HttpContext.Current != null)
{
var bll = HttpContext.Current.Items[key] as T;
lock (key)
{
if (bll == null)
{
bll = new T();
HttpContext.Current.Items[key] = bll;
}
}
return bll;
}
else
{
return new T();
}
}
}
在UI或確定是有HTTP請求的上下文中都可以這樣調(diào)用了(當(dāng)然,如果基于其他上下文,也可以寫一個類似上面的實例化方法):
User u = B.Entity<User>();
到止完結(jié),平時寫文章不多,表達欠佳,望親們見諒!