MongoDB.WebIDE:升级版的Mongodb管理工具

很早以前收藏了一片文章:《强大的MongoDB数据库管理工具》。最近刚好要做一些MongoDB方面的工作,于是翻出来温习了一下,用起来也确实挺方便。不过在使用过程中出现了一些个问题,加上更喜欢MongoUVE的操作习惯,于是决定“自己动手,丰衣足食”,写一个升级版的工具。

一、升级的基础内容
  1. 原版是用的WebForm开发的,新版打算升级到MVC
  2. 前端框架使用bootstrap
  3. Mongodb的驱动程序改为使用官方版的驱动程序
  4. 原版查询数据时使用的是解析sql语句的方式,新版打算采用javascript shell命令
  5. 原有的功能点都进行一些辅助性的改进

下面在对各个功能介绍的过程中,对升级的细节进行逐一的介绍,改动的地方会增加下划线进行标注。

二、系统主界面

系统主界面如下:

主界面

主界面采用了bootstrap的栅格系统进行布局:顶部为banner导航,左侧为Mongo对象的树菜单区域,右侧为功能操作的主区域。点击顶部banner和左侧上部的刷新图标均能重新加载主界面。主区域默认进入服务器管理的界面。

<div class="row-fluid">
    <div class="span3 site-left">
        <div class="form-inline site-left-top">
            <i class="icon-list"></i>&nbsp;<span>
            <a href="/Home/ServerManager/" target="mainFrame">Servers</a></span>&nbsp;
            <a action="refresh"><i class="icon-refresh"></i></a>&nbsp;
            <a action="expand"><i class="icon-chevron-down"></i></a>&nbsp;
            <a action="collapse"><i class=" icon-chevron-up"></i></a>
        </div>
        <div id="menu">
            <ul id="tree" class="ztree">
            </ul>
        </div>
    </div>
    <div class="span9 pr10">
        <iframe id="mainFrame" name="mainFrame" height="100%" width="100%" frameborder="0" src="/Home/ServerManager/"></iframe>
    </div>
</div>
View Code

本例中,树菜单里的测试服务器有两个,是在本机通过不同端口建立的两个MongoDB实例,用来进行主从库的模拟(这个在后面会用到)。

三、Mongo对象的树菜单

首先,树菜单采用了zTree控件代替了原来的TreeView控件。zTree是一个非常好用的树控件,它支持json数据绑定,支持异步加载,对节点的控制和操作都非常强大,甚至可以直接在树节点上编辑数据。个人非常推荐这个控件。点击这里可以进入zTree控件的官网。

其次,Mongo对象的采用了“服务器 》数据库 》 数据表 》 字段和索引”的层次结构。因为字段和索引都处在表节点的下级,因此加入了两个虚拟节点【表信息】和【索引】来进行分类组织。并且点击不同的对象节点会触发不同的操作。

按照MongoUVE的表现形式,表节点右边增加了该表的数据总量的显示,字段节点右边增加了该字段的数据类型的显示。具体界面如下图:

树菜单

系统定义了树节点的数据结构:MongoTreeNode,用来生成绑定树控件的json数据。每个节点对应的对象类型也定义了对应的枚举:MongoTreeNodeType。代码如下:

/// <summary>
    /// 树节点
    /// </summary>
    public class MongoTreeNode
    {
        /// <summary>
        /// 唯一值
        /// </summary>
        public uint ID { get; set; }
        /// <summary>
        /// 父节点ID
        /// </summary>
        public uint PID { get; set; }
        /// <summary>
        /// 名称
        /// </summary>
        public string Name { get; set; }
        /// <summary>
        /// 类型
        /// </summary>
        public MongoTreeNodeType Type { get; set; }
    }

/// <summary>
    /// 节点类型
    /// </summary>
    public enum MongoTreeNodeType
    {
        /// <summary>
        /// 服务器
        /// </summary>
        [Description("服务器")]
        Server = 1,
        /// <summary>
        /// 数据库
        /// </summary>
        [Description("数据库")]
        Database = 2,
        /// <summary>
        /// 数据表
        /// </summary>
        [Description("数据表")]
        Collection = 3,
        /// <summary>
        /// 字段
        /// </summary>
        [Description("字段")]
        Field = 4,
        /// <summary>
        /// 索引
        /// </summary>
        [Description("索引")]
        Index =5,
        /// <summary>
        /// 表信息填充节点
        /// </summary>
        [Description("表信息填充节点")]
        TableFiller = 6,
        /// <summary>
        /// 索引填充节点
        /// </summary>
        [Description("索引填充节点")]
        IndexFiller = 7
    }
View Code

zTree控件在绑定之前需要对各种属性进行预定义,详情请参考zTree控件的API手册。在本系统中预制了ID和PID字段作为父子关系绑定的字段,而控件默认值是id和pId字段。

var setting = {
        view: {
            dblClickExpand: false,
            showLine: true,
            selectedMulti: false
        },
        data: {
            key: {
                name: "Name"
            },
            simpleData: {
                enable: true,
                idKey: "ID",
                pIdKey: "PID",
                rootPId: ""
            }
        },
        callback: {
            onClick: nodeClick
        }
    };
View Code

*注意:在实际使用的过程中,在调用树控件的绑定方法时,偶尔还会报“找不到绑定字段ID”的错,导致树控件无法显示。这个问题有待解决。

系统还定义了Mongo对象的数据结构,主要有“服务器、数据库、数据表 、字段和索引”五种(具体见代码)。对象的ID字段分别对应于树节点的ID和PID字段,即数据库对象的ID,既对应数据库节点的ID字段,又对应表节点的PID字段。对这些对象的结构定义,比原版系统精简了很多。

在读取数据表对象时,过滤了一些系统表和有特殊意义的表(如索引表,profile数据表)。如果需要放开显示的话,可以去程序中进行屏蔽。代码如下:

collections.Where(t => !t.Contains("$") && !t.Contains(MongoConst.IndexTableName) && !t.Contains(MongoConst.ProfileTableName)).ToList().ForEach(t =>
{
       ...
}
View Code

有一个改进是:对象ID字段的数据类型从原来的GUID改为uint类型(为了保证数值大于0,切避免出现正值和负值取绝对值后重复的可能性,才采用无符合数),以提高查找效率并减小json数据的大小。ID字段是由程序自动生成的唯一的随机值,它是在一个并行计算(之后会讲到)过程中得到的。为了保证在并行环境中随机数的唯一性,参考了《如何在高并发分布式系统中生成全局唯一Id》这篇文章。最后形成的代码很简单,经过测试到目前为止还没有出现过ID值重复的情况。代码如下:

/// <summary>
        /// 随机数的步长
        /// </summary>
        private static readonly int StepLength = 8;
        /// <summary>
        /// 生成随机数
        /// </summary>
        /// <returns></returns>
        public static uint GetRandomId()
        {
            var bytes = new byte[StepLength];
            var provider = new RNGCryptoServiceProvider();
            provider.GetNonZeroBytes(bytes);

            return BitConverter.ToUInt32(bytes, 0);
        }
View Code

Mongo对象和树节点是同一时间读取的,在第一次加载完后就会保存至缓存中,缓存时间为2小时。当缓存超时后再点击树菜单节点或进行其他操作时,在主区域都会出现如下的界面,要求重新加载数据:

缓存超时

上面提到的并行计算的方法,是在每次从数据库中加载数据时才会调用,这也是本次一大改进。通过采用并行计算,确实提高了读取数据的效率,数据库内对象越多越明显。关于并行计算的学习,我推荐《8天玩转并行开发》这一系列的文章,非常好。新版系统的代码里保留了循环读取Mongo对象的代码,有兴趣的朋友,可以自己修改一下程序去体会一下并行和循环的差别。为了保证在并行计算中的线程安全,树节点采用HashSet进行缓存,HashSet本身是线程安全的;而Mongo对象则采用Hashtable进行缓存,在初始化时,需要使用Synchronized方法来保证线程安全。

public HashSet<MongoTreeNode> TreeNodes { get; set; }
        public Hashtable MongoObjects { get; set; }

        public MongoContext()
        {
            TreeNodes = new HashSet<MongoTreeNode>();
            MongoObjects = Hashtable.Synchronized(new Hashtable());

            GetServer();
        }
View Code
四、服务器管理

服务器管理界面在初始加载时就会显示在主区域内。通过点击左侧顶部的【Servers】链接,也能操作服务器管理。主区域的操作界面,统一采用了bootstrap中的折叠插件来进行展示,点击不同的折叠栏,会显示该栏的内容同时折叠其他的内容。

服务器列表

服务器添加

服务器管理功能,实质是修改程序里【Config\servers.config】的文件,为其添加或删除节点。文件里保存的数据,作为并行计算读取数据的起点。

五、服务器信息统计

点击树菜单中的服务器节点,就会在主区域内显示该服务器的统计信息。在主区域的顶部,会显示服务器对象的一些基本信息。本系统中,大量的信息展示均使用zTree控件进行展现,因此重用了MongoTreeNode的结构,不同的地方仅在于不需要为MongoTreeNodeType属性赋值。

服务器统计信息

查看服务器的统计信息,实际对应于MongoDB的adminCommand命令:db.adminCommand{"serverStatus", 1}。新版系统实现如下:

public override List<MongoTreeNode> GetInfo()
        {
            var mongo = new MongoClient(string.Format(MongoConst.ConnString, Server.Name));
            var server = mongo.GetServer();
            var adminDB = server.GetDatabase(MongoConst.AdminDBName);
            var rst = adminDB.RunCommand(new CommandDocument { { "serverStatus", 1 } });

            var list = new List<MongoTreeNode>();
            if (rst.Ok)
            {
                BuildTreeNode(list, 0, rst.Response);
            }
            return list;
        }
View Code

这里简单介绍一下MongoDB的BsonDocument。详情请移步至官方文档: C# and .Net Mongo Driver

BsonDocument是官方驱动进行各种操作的一个基础数据,它是一个键/值对的集合,有三种生成方式:

  1. 使用Add或者Set方法
    var book = new BsonDocument();
    book.Add("author", "Ernest Hemingway");
    book.Add("title", "For Whom the Bell Tolls");
    View Code
  2. 使用Add或者Set的流式接口方法
    var book = new BsonDocument().Add("author", "Ernest Hemingway").Add("title", "For Whom the Bell Tolls");
    View Code
  3. 使用C#的类初始化语法(推荐使用此种方法)
    var book = new BsonDocument {
        { "author", "Ernest Hemingway" },
        { "title", "For Whom the Bell Tolls" }
    };
    View Code

基于BsonDocument派生出很多子类,包括:

  • CommandDocument:用于运行命令的参数文档,在RunCommand方法中使用
  • FieldsDocument:用于查询返回字段的参数文档,在SetFields方法中使用
  • GroupByDocument:用于group的参数文档(这个的用处暂时没体会得到)
  • QueryDocument用于设置查询条件的参数文档,在Find方法中使用
  • SortByDocument:用于设置排序规则的参数文档,在SetSortOrder方法中使用
  • UpdateDocument:用于修改数据的参数文档,在Update方法中使用

其他子类和它们的具体使用方法,需要在使用过程中去体会。一般来说,什么样的类型就对应什么样的操作方法。在方法调用的参数接口说明中,都会有比较明显的标识。

六、服务器的主从同步信息

在文章的第一节说明了在本机搭建有主从环境,其中127.0.0.1:27017是主服务器,其中127.0.0.1:28018是从服务器。通过点击【查看同步信息】链接,可以进入查看同步信息的页面。

主服务器信息:

主服务器

主服务器的同步日志,新版系统设定为仅显示10条记录,可以自己进行修改:

同步日志

从服务器信息:

从服务器

从服务器的源信息:

从服务器的同步信息

服务器的主从信息也都是使用adminCommand命令来获取。主服务器的同步日志则是从local库中的【oplog.$main】表中读取,从服务器的源信息则是从local库中的【sources】表读取。实现代码:

//服务器的主从信息
var stats = adminDB.RunCommand(new CommandDocument { { "ismaster", 1 } });

var localDB = server.GetDatabase(MongoConst.LocalDBName);
//主服务器的同步日志
var docs = localDB.GetCollection(MongoConst.OplogTableName).FindAll().SetLimit(10);
//从服务器的源信息
var doc = localDB.GetCollection(MongoConst.SourceTableName).FindOne();
View Code
七、数据库的统计信息

点击树菜单中的数据库节点,就会在主区域内显示该数据库的统计信息。在主区域的顶部,会显示数据库对象的一些基本信息。

数据库统计信息

数据库的统计信息是使用RunCommand命令来获取,因为是查看某个具体数据库的统计信息,因此不需要使用Admin库,即不使用adminCommand。代码如下:

public override List<MongoTreeNode> GetInfo()
        {
            var mongo = new MongoClient(string.Format(MongoConst.ConnString, Server.Name));
            var server = mongo.GetServer();
            var db = server.GetDatabase(Database.Name);
            var rst = db.RunCommand(new CommandDocument { { "dbstats", 1 } });

            var list = new List<MongoTreeNode>();
            if (rst.Ok)
            {
                BuildTreeNode(list, 0, rst.Response);
            }
            return list;
        }
View Code
八、数据库的Profile优化

在数据库的统计信息页面点击【Profile优化】的链接,即进入数据库的Profile设置的界面。Profile的状态一共有三种:

  • 不开启:即不记录pofile数据,这样就无法进行效率分析
  • 记录慢命令:仅记录执行速度慢的profile数据,可以自行设置判断是否是慢命令的执行时间的临界值,一般推荐此种方式
  • 记录所有命令:记录所有执行的profile数据,因为记录profile数据也需要系统资源和开销,因此不推荐开启记录所有命令的方式

设置profile

查看Profile数据,可以自行调整显示的条数。官方驱动返回的数据类型是 SystemProfileInfo大家可以查看源代码获得详细的信息。在本系统中选取了其中几个属性进行了一下转换,并采用更直观的列表形式来进行展示

proflie数据

优化的一些小建议:

  1. 当【扫描记录数】远大于【返回记录数】,可以考虑通过增加索引来优化查询效率
  2. 当【返回结果集】过大,那么说明我们返回的结果集太大了,可以考虑只返回所需要的字段
  3. 如果写查询量或者update量过大的话,多加索引是会有好处的;执行 update 操作时同样检查一下【扫描记录数】,并使用索引减少文档扫描数量
  4. 如果读操作很少,那么尽量不要添加索引,因为索引越多,写操作会越慢;如果读操作很多,则需要考虑创建合适的索引
九、数据表的统计信息

点击树菜单中的数据表节点,就会在主区域内显示该数据表的统计信息。在主区域的顶部,会显示数据表对象的一些基本信息。

数据表信息

数据表的统计信息是使用RunCommand命令来获取,需要在某个具体的数据库上执行该命令,并需要传入对应表的名称。代码如下:

public override List<MongoTreeNode> GetInfo()
        {
            var mongo = new MongoClient(string.Format(MongoConst.ConnString, Server.Name));
            var server = mongo.GetServer();
            var db = server.GetDatabase(Database.Name);
            var rst = db.RunCommand(new CommandDocument { { "collstats", Table.Name } });

            var list = new List<MongoTreeNode>();
            if (rst.Ok)
            {
                BuildTreeNode(list, 0, rst.Response);
            }
            return list;
        }
View Code
十、查看数据

在数据表统计信息的页面点击【查看数据】链接,或者是点击树菜单中的【表信息】节点,均可以进入数据查看页面。注意:【表信息】的子节点是字段节点,点此节点不做任何操作。

查看数据

数据查询的操作方式抛弃了原版系统解析sql语句的方式,采用了类似MongoUVE的操作形式,输入json格式的命令进行查询。新版系统可设置【find、sort、skip、limit】的参数。查询结果采用列表方式展示,表头内显示字段名以及该字段的数据类型。在显示数据时,当数据为BsonDocument类型时,做了序列化字符串的处理。

查询结果

在实现利用javascript shell命令进行查询时,差点绕了一个大圈子:一开始觉得需要做一个javascript shell命令解析器,再配合MongoDB.Driver.Builders.Query类来生成查询用的QueryDocument。一开始做了一个or命令的解析,例如:{$or:[{Word:'汶川县'},{Pos:{$gt:256}}]},觉得太难了,各种嵌套怎么解析得完?官方驱动怎么可能对javascript shell命令的支持这么差?于是开始翻源代码,突然看到一个方法:BsonDocument.Parse!!然后一切都容易了。查询实现的方法如下:

public List<BsonDocument> GetData(string jsonfind, string jsonsort, int skip, int limit)
        {
            var mongo = new MongoClient(string.Format(MongoConst.ConnString, Server.Name));
            var server = mongo.GetServer();
            var db = server.GetDatabase(Database.Name);                        
            
            var findDoc = string.IsNullOrEmpty(jsonfind) ? new QueryDocument() : new QueryDocument(BsonDocument.Parse(jsonfind));
            var sortDoc = string.IsNullOrEmpty(jsonsort) ? new SortByDocument() : new SortByDocument(BsonDocument.Parse(jsonsort));
            var query = db.GetCollection(Table.Name).Find(findDoc);      
            query.SetSortOrder(sortDoc);
            if (skip > 0)
            {
                query.Skip = skip;
            }
            if (limit > 0)
            {
                query.Limit = limit;
            }
            return query.ToList();
        }
View Code

新版系统增加了查看执行计划的功能。在此可对查询语句的执行效率进行分析,从而协助进行系统优化。目前仅支持【find、sort】的参数设定。

执行计划

 执行计划结果

十一、索引管理

在数据表统计信息的页面点击【查看索引】链接,或者是点击树菜单中的【索引】节点,可进入索引管理页面。该页面提供索引列表展示,删除索引以及添加索引的功能。注意:【索引】的子节点是索引节点,点此节点不做任何操作。

索引列表

新增索引时,可以使用多个字段生成组合索引。目前不支持当某个字段为文档类型时,使用内部字段设置索引的情况。另外索引在创建时的一些选项的意义也做了相应的说明。

索引管理

十二、后记

对于这个新版系统上面介绍了很多,目前还缺少一部分的功能,例如数据的新增和修改的操作;Mapreduce的支持;甚至自定义javascript shell命令的执行(类似执行自定义的sql语句)等……欠缺的功能以后有时间再慢慢补上来的。

在介绍的过程中也穿插的展示了一部分代码,但没有对系统的代码进行完整的介绍,理由是整套代码几乎没有难度,况且代码就是最好的文档,而整个系统的代码量并不大。开发过程主要是加深对MongoDB的一些概念的认识,再配合官方驱动的各种方法进行佐证。

那么,最后,问题来了……代码在哪里呢?中国XX找XX吗?当然不是,代码下载请点下图:

下载

posted @ 2014-10-18 00:26  芈老头  阅读(11943)  评论(13编辑  收藏  举报