zoukankan      html  css  js  c++  java
  • RavenDB在传统C/S应用下的一点实践

    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}
                        }
                    });
                }
            }
    
    分享到: 更多
  • 相关阅读:
    xftp无法用root账号登录问题
    jenkins上gradle打包
    jenkins登录后页面显示为空的问题
    sql 修改oracle数据库中某个字段的部分内容
    redis安装及报错处理
    Centos7 firewall-cmd not found
    ftp connect: No route to host 解决方案
    反向代理负载均衡之Apache
    centos7 openldap双主部署
    apache安装以及报错处理
  • 原文地址:https://www.cnblogs.com/redmoon/p/2842725.html
Copyright © 2011-2022 走看看