RavenDB在傳統C/S應用下的一點實踐

來源:互聯網
上載者:User
文章目錄
  • RavenDBDataSource解析和用法
  • 如何儲存資料變更
RavenDB介紹

RavenDB是一個基於.NET開發的NoSQL資料庫。下面是官方介紹的一個簡單翻譯:

RavenDB is a transactional, open-source Document Database written in .NET, offering a flexible data model designed to address requirements coming from real-world systems.

RavenDB allows you to build high-performance, low-latency applications quickly and efficiently.

RavenDB是一個用.NET編寫的事務性開來源文件資料庫,提供靈活的資料模型,設計用於解決來自真實世界系統的需求。

RavenDB允許你快速而高效地構建高效能、低延遲的應用程式。

更多介紹可以瀏覽官方網站的介紹:http://ravendb.net/features

情境介紹

由於NoSQL一般是用於Web情境,比如Web應用程式(尤其MVC Web應用程式),或者Web服務(包括REST服務等)。最近,需要實現一個簡單的資料編輯工具,不過由於某些原因,這個工具必須和一個案頭的Windows Forms應用程式整合在一起,且也要滿足多個使用者同時操作資料的需求。對於這種標準的C/S模式的應用,能否使用RavenDB這樣的NoSQL來作為Server端的資料庫呢?

答案當然是可以的。畢竟RavenDB本身就支援兩種運行模式:嵌入模式(Embedded)和伺服器模式(Server)。對於C/S的應用,很自然就是把RavenDB部署在一個伺服器上,運行於Server模式,然後在用戶端通過.NET Client API來訪問。

遇到問題

在這個C/S應用程式中使用RavenDB的過程中,遇到的最大的問題,還是RavenDB本身的一些特性所帶來的限制,分別為:

  1. 每次擷取的資料量有限制。RavenDB規定每次擷取的資料量預設為128條,最多可配置為1024條。對於我這個工具的資料量,就是5000條左右,其實如果使用其他資料庫技術的話(比如Entity Framework),且也在區域網路內,完全可以一次性載入到記憶體中。然而使用RavenDB就必須考慮分頁處理。
  2. 每個Session能夠調用的次數有限制。RavenDB規定每個Session調用服務端的最大次數是30,並且推薦最好控制在1次左右。由於有這樣的規定,就無法在整個用戶端應用程式的生存期內保持一個共用的session。 對於EF也不存在這樣的限制。
  3. 搜尋是基於Lucene的。對於字串進行Contain操作會出錯,這是由於對於類似的全文檢索搜尋,RavenDB都是依賴於Lucene的。因而需要預先定義搜尋的索引,並使用單獨的Search方法。
  4. RavenDB內建的Lucene分詞器對於中文的支援有問題。就需要單獨使用其他中文分詞器。
解決方式

針對以上的限制,並結合我這個C/S小工具的一些特點,使用了如下解決方式:

  1. 結合BindingNavigator和BindingSource,編寫了一個自動分頁的工具類(RavenDBDataSource),可以讓BindingNavigator的前後導覽按鈕實現分頁導航,還可以支援條件過濾(Where)和全文檢索搜尋(Search)後的分頁。具體用法見下“RavenDBDataSource解析和用法”。
  2. 雖然不能保持一個共用的Session,但是可以保持一個共用的Store對象,在每次需要擷取資料或更新資料的時候,建立單獨的Session。不過需要注意的是,由於沒有共用Session,會導致之前取回的資料丟失變更跟蹤,需要自己進行跟蹤與提交。見下面的“如何儲存資料變更”。
  3. 我從Lucene.NET的網站下載了Contri包,直接使用了裡面的“Lucene.Net.Analysis.Cn.ChineseAnalyzer”,即把Lucene.Net.Contrib.Analyzers.dll檔案放到RavenDB\Server\Analyzers目錄裡面。把當然有興趣的同學也可以使用ICTCLAS的Lucene實現。
  4. 預定義全文檢索搜尋索引的話,我的方式是在串連資料庫後,檢查是否存在所需索引,不存在就用代碼建立。當然也可以通過Studio來建立。見下”建立索引”。
RavenDBDataSource解析和用法

代碼見:https://github.com/heavenwing/redmoon/blob/master/RavenDBDataSource.cs

這個類提供了一個構造器public RavenDBDataSource(IDocumentStore store, BindingNavigator bn, BindingSource bs),可以接受IDocumentStore 、BindingNavigator 和BindingSource 作為參數。其中會對bn進行一些初始化處理。

提供了一個重載的Load方法,可以無參數,或者接受Func<IRavenQueryable<T>, IRavenQueryable<T>> criteria, string indexName = ""兩個參數。criteria用來對查詢進行構造,indexName顧名思義,在進行Search操作的時候就需要傳入預先定義的索引的名稱。在Load方法中,會對調用代碼構造好的查詢進行執行,根據PageSize的設定進行分頁查詢,把查詢結果賦值給BindingSource來提示和BindingSource綁定的控制項(如DataGridView)進行重新整理。在進行分頁查詢的同時,也會更新當前的頁碼。

其中BindingNavigator 對象的PositionItem的TextChanged事件處理,會觸發Load事件。為了避免頻率過高的執行,我使用了一個自訂的事件延遲器(見:https://github.com/heavenwing/redmoon/blob/master/DelayEvent.cs),當然也可以使用RX來進行延遲。

具體用法就很簡單:執行個體化一個用於具體實體類的RavenDBDataSource,然後調用Load方法,在Load方法中構造查詢。如:

        private void LoadProcessData()        {            if (_dsProcess == null)                _dsProcess = new RavenDBDataSource<ProcessEntity>(_store, bnProcess, bsProcess);            var txt = tstbSearchForProcess.Text.ToLower();            if (string.IsNullOrEmpty(txt))            {                if (tscbSource.SelectedIndex == 0)                {                    if (tscbRelatedCount.SelectedIndex < 5)                        _dsProcess.Load(query => query                            .Where(o => o.RelatedCount == tscbRelatedCount.SelectedIndex)                            .OrderBy(o => o.ProductName));                    else                        _dsProcess.Load(query => query                            .Where(o => o.RelatedCount >= 5)                            .OrderBy(o => o.ProductName));                }                else                {                    if (tscbRelatedCount.SelectedIndex < 5)                        _dsProcess.Load(query => query                            .Where(o => o.Source == tscbSource.Text                             && o.RelatedCount==tscbRelatedCount.SelectedIndex)                            .OrderBy(o => o.ProductName));                    else                    {                        _dsProcess.Load(query => query                            .Where(o => o.Source == tscbSource.Text                            && o.RelatedCount >= 5)                            .OrderBy(o => o.ProductName));                    }                }            }            else            {                _dsProcess.Load(query => query                        .Search(o => o.ProductName, txt)                        .OrderBy(o => o.ProductName),                        index1Name                   );            }        }

上述代碼中,可以同時對多個屬性進行過濾(Where),也可通過設定索引名稱(index1Name)對一個或多個屬性進行搜尋(Search)。

另外,為了方便一次性擷取某個實體的所有資料,這個類額外提供了一個方法public static List<T> LoadAll(IDocumentSession session, int pageSize),可以由外部提供一個session以便對擷取的所有資料都進行變更跟蹤。用法如下:

 using (var session = _store.OpenSession())                    {                        var processes = RavenDBDataSource<ProcessEntity>.LoadAll(session, 512);                        var products = RavenDBDataSource<ProductEntity>.LoadAll(session, 512);                        foreach (var process in processes)                        {                            var count = 0;                            foreach (var product in products)                            {                                foreach (var dataset in product.Datasets)                                {                                    if (process.Id == dataset.Id)                                        count++;                                }                            }                            if (process.RelatedCount != count)                                process.RelatedCount = count;                        }                        session.SaveChanges();                    }
如何儲存資料變更

對於C/S的應用,可能會需要時常進行儲存操作,因而在RavenDB的限制條件下,無法維持一個共用的Session,由於沒有共用的Session,導致無法對當前顯示到UI的資料進行變更跟蹤,由於沒有變更跟蹤,對資料進行儲存就只有採用如下三種方式的一種:

  1. 如果可以獲得到某個實體的執行個體,比如BindingSource的某條資料,那麼可以使用session.Store(process, id)來儲存,並調用SaveChanges;
  2. 如果只能擷取到實體的id,那麼只能先Load實體的執行個體對象,對其中的屬性進行編輯,並調用SaveChanges;
  3. 如果只能擷取到實體的id,且實體相對比較龐大(或者不想先Load)的話,可以使用Patching API進行部分更新。

注意,以上用到的id並不是實體本身的Id屬性,以ProcessEntity為例,是var id = string.Format("ProcessEntities/{0}", process.Id);

對於上述三種方式的選擇,首選第1種,而部分更新由於不會歸到事務中在SaveChanges中統一提交,所以一般不被推薦。

另外在這樣的限制條件下對於刪除操作,可以採用如下兩種方式:

  1. 先通過id來Load實體的執行個體對象,然後使用session.Delete(entity)刪除,並調用SaveChanges;
  2. 或者使用session.Advanced.Defer(new DeleteCommandData { Key = id })來刪除,並調用SaveChanges;

對於刪除而言,優選第2種方式,次選第1種方式,畢竟Defer方法的真正執行,是要放到SaveChanges中統一提交的,且不用去載入實體的內容。

建立索引

我的方式是自己用代碼來建立,建立一個方法,在store初始化後,就調用,代碼應該一目瞭然:

        const string AnalyzerName = "Lucene.Net.Analysis.Cn.ChineseAnalyzer, Lucene.Net.Contrib.Analyzers, Version=3.0.3.0, Culture=neutral, PublicKeyToken=85089178b9ac3181";        const string index1Name = "ProcessEntities/ByProductName";        const string index2Name = "ProductEntities/ByZhNameAndEnName";        private void SetDocumentIndex()        {            var index = _store.DatabaseCommands.GetIndex(index1Name);            if (index == null)            {                _store.DatabaseCommands.PutIndex(index1Name,                new IndexDefinitionBuilder<ProcessEntity>                {                    Map = processes => from p in processes                                       select new                                       {                                           p.ProductName,                                       },                    Indexes =                    {                        {o=>o.ProductName,FieldIndexing.Analyzed},                    },                    Analyzers =                    {                        {o=>o.ProductName,AnalyzerName}                    }                });            }            index = _store.DatabaseCommands.GetIndex(index2Name);            if (index == null)            {                _store.DatabaseCommands.PutIndex(index2Name,                new IndexDefinitionBuilder<ProductEntity>                {                    Map = processes => from p in processes                                       select new                                       {                                           p.EnName,                                           p.ZhName                                       },                    Indexes =                    {                        {o=>o.EnName,FieldIndexing.Analyzed},                        {o=>o.ZhName,FieldIndexing.Analyzed},                    },                    Analyzers =                    {                        {o=>o.EnName,AnalyzerName},                        {o=>o.ZhName,AnalyzerName}                    }                });            }        }
相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.