使用MongoDB作为后台数据库的尝试

MongoDB作为一个阶层型数据库,在很短的时间里面是不可能被大面积推广使用的,

本文作为一个实验性的课题,探讨一下MongoDB作为网站数据库的可能性。

1.MongoDB作为代替关系型数据库的可能性。

2.MongoDB作为代替文件服务器的可能性。

通过探讨来加强对于MongoDB的认识

 

环境准备

技术选型

1.由于是验证性质的课题,这里没有使用MVC5/6.如果有人对MVC6有兴趣,可以另开一个课题讨论。这里使用的是传统的WebForm。

2.使用MongoDB最新版本作为数据库

3.MongoDB的GUI使用本人自己开发的工具

4.项目Host在阿里云上

截图1.

阿里云上的MongoDB数据库,使用本人开发工具看到的结果图

 

5.启用  meishiyouji.com 域名

6.UI使用的是  UIKit  这个库,进行响应式布局

 

7.使用官方的MongoDB的C#驱动程序

 

开发实践

1.MongoDB作为阶层型数据库的代表,其最大特点就是Free的存储方式,一个Collection(相当于数据表)里面可以存储各种结构的文档。

这个特性,如果用得好,非常有帮助,如果用的不好,容易造成混乱。例如我们可以将继承同一个基类的子类实例放在同一个Collection里面,

这个是传统数据库无法做的,不同的子类的字段数会不一样,无法放到同一个表里面去,但是MongoDB是可以做到的。

 

例子:

基类如下

 例如一个行程的详细信息的基类如下:

  1 using Common.Database;
  2 using Common.Misc;
  3 using MongoDB.Bson.Serialization.Attributes;
  4 using MongoDB.Driver.Builders;
  5 using System.Collections.Generic;
  6 
  7 namespace Common.Schedule
  8 {
  9     [BsonKnownTypes(typeof(Traffic), typeof(Visit), typeof(Shopping), typeof(Catering))]
 10     public class DetailScheduleInfoBase : EntityBase
 11     {
 12         /// <summary>
 13         /// 数据集名称
 14         /// </summary>
 15         public static string CollectionName = "DetailScheduleInfo";
 16         /// <summary>
 17         /// 序列号前缀
 18         /// </summary>
 19         public const string Prefix = "DI";
 20         /// <summary>
 21         /// 
 22         /// </summary>
 23         public static string NewSN = Prefix + 0.ToString("D8");
 24         /// <summary>
 25         /// OverviewSN
 26         /// </summary>
 27         public string OverviewSN = string.Empty;
 28         /// <summary>
 29         /// DiarySchduleSN
 30         /// </summary>
 31         public string DiarySchduleSN = string.Empty;
 32         /// <summary>
 33         /// 说明
 34         /// </summary>
 35         public string Description = string.Empty;
 36         /// <summary>
 37         /// Visit
 38         /// </summary>
 39         public string Location = string.Empty;
 40         /// <summary>
 41         /// 移动开始时间
 42         /// </summary>
 43         public string FromTime = string.Empty;
 44         /// <summary>
 45         /// 移动终止时间
 46         /// </summary>
 47         public string ToTime = string.Empty;
 48         /// <summary>
 49         /// 分类
 50         /// </summary>
 51         public SchduleTypeEnum SchduleType = SchduleTypeEnum.Other;
 52         /// <summary>
 53         /// 图片
 54         /// </summary>
 55         public string UploadImgUrl = string.Empty;
 56         /// <summary>
 57         /// 分类中文
 58         /// </summary>
 59         /// <param name="c"></param>
 60         /// <returns></returns>
 61         public static string GetSchduleTypeEnumChinese(SchduleTypeEnum c)
 62         {
 63             string SchduleTypeEnumChinese = string.Empty;
 64             switch (c)
 65             {
 66                 case SchduleTypeEnum.Traffic:
 67                     SchduleTypeEnumChinese = "交通";
 68                     break;
 69                 case SchduleTypeEnum.Visit:
 70                     SchduleTypeEnumChinese = "参观";
 71                     break;
 72                 case SchduleTypeEnum.Shopping:
 73                     SchduleTypeEnumChinese = "购物";
 74                     break;
 75                 case SchduleTypeEnum.Catering:
 76                     SchduleTypeEnumChinese = "饮食";
 77                     break;
 78                 //case SchduleTypeEnum.Rest:
 79                 //    SchduleTypeEnumChinese = "休息";
 80                 //    break;
 81                 //case SchduleTypeEnum.Certificates:
 82                 //    SchduleTypeEnumChinese = "手续";
 83                 //    break;
 84                 case SchduleTypeEnum.Other:
 85                     SchduleTypeEnumChinese = "其他";
 86                     break;
 87                 default:
 88                     break;
 89             }
 90             return SchduleTypeEnumChinese;
 91         }
 92         /// <summary>
 93         /// 分类枚举
 94         /// </summary>
 95         public enum SchduleTypeEnum
 96         {
 97             /// <summary>
 98             /// 交通
 99             /// </summary>
100             Traffic,
101             /// <summary>
102             /// 参观
103             /// </summary>
104             Visit,
105             /// <summary>
106             /// 饮食
107             /// </summary>
108             Catering,
109             /// <summary>
110             /// 购物
111             /// </summary>
112             Shopping,
113             ///// <summary>
114             ///// 休息
115             ///// </summary>
116             //Rest,
117             ///// <summary>
118             ///// 手续
119             ///// </summary>
120             //Certificates,
121             /// <summary>
122             /// 其他
123             /// </summary>
124             Other
125         }
126         /// <summary>
127         /// 备注
128         /// </summary>
129         public string Comments = string.Empty;
130         /// <summary>
131         /// 是否为一个备选方案
132         /// </summary>
133         public bool IsBack = false;
134         /// <summary>
135         /// 消费记录
136         /// </summary>
137         public List<Consumption.ConsumptionAbstract> ConsumptionList = new List<Consumption.ConsumptionAbstract>();
138         /// <summary>
139         /// 添加详细行程
140         /// </summary>
141         /// <param name="info"></param>
142         public static void Insert(DetailScheduleInfoBase info, string CreateUserName)
143         {
144             Operater.InsertRec(Prefix, CollectionName, info, CreateUserName);
145         }
146         /// <summary>
147         /// 通过序列号获得概要对象
148         /// </summary>
149         /// <param name="SN"></param>
150         public static DetailScheduleInfoBase GetDetailScheduleBySN(string DetailScheduleSN)
151         {
152             var query = Query.EQ("_id", DetailScheduleSN);
153             return Operater.GetFirstRec<DetailScheduleInfoBase>(CollectionName, query);
154         }
155         /// <summary>
156         /// 更新概要对象
157         /// </summary>
158         /// <param name="newobj"></param>
159         public static void Update(DetailScheduleInfoBase newobj, string UserName)
160         {
161             Operater.UpdateRec(CollectionName, newobj, UserName);
162         }
163     }
164 }

 某一个子类的代码如下

 1 namespace Common.Schedule
 2 {
 3     public class Traffic : DetailScheduleInfoBase
 4     {
 5         public Traffic()
 6         {
 7             SchduleType = SchduleTypeEnum.Traffic;
 8         }
 9         /// <summary>
10         /// 移动开始地址
11         /// </summary>
12         public string MoveFromLocation = string.Empty;
13         /// <summary>
14         /// 移动终止地址
15         /// </summary>
16         public string MoveToLocation = string.Empty;
17         /// <summary>
18         /// 交通手段
19         /// </summary>
20         public TrafficTypeEnum TrafficType = TrafficTypeEnum.Walk;
21         /// <summary>
22         /// 交通手段枚举
23         /// </summary>
24         public enum TrafficTypeEnum
25         {
26             /// <summary>
27             /// 步行
28             /// </summary>
29             Walk,
30             /// <summary>
31             /// 出租
32             /// </summary>
33             Taxi,
34             /// <summary>
35             /// 巴士
36             /// </summary>
37             Bus,
38             /// <summary>
39             /// 捷运
40             /// </summary>
41             JieYun,
42             /// <summary>
43             /// 台铁
44             /// </summary>
45             Train,
46             /// <summary>
47             /// 高铁
48             /// </summary>
49             SpeedTrain,
50             /// <summary>
51             /// 飞机
52             /// </summary>
53             AirPlane,
54             /// <summary>
55             /// 轮船
56             /// </summary>
57             Ship
58         }
59         /// <summary>
60         /// 交通手段中文
61         /// </summary>
62         /// <param name="c"></param>
63         /// <returns></returns>
64         public static string GetTrafficTypeEnumChinese(TrafficTypeEnum c)
65         {
66             string CurrencyEnumChinese = string.Empty;
67             switch (c)
68             {
69                 case TrafficTypeEnum.Walk:
70                     CurrencyEnumChinese = "步行";
71                     break;
72                 case TrafficTypeEnum.Taxi:
73                     CurrencyEnumChinese = "出租";
74                     break;
75                 case TrafficTypeEnum.Bus:
76                     CurrencyEnumChinese = "巴士";
77                     break;
78                 case TrafficTypeEnum.JieYun:
79                     CurrencyEnumChinese = "捷运";
80                     break;
81                 case TrafficTypeEnum.Train:
82                     CurrencyEnumChinese = "台铁";
83                     break;
84                 case TrafficTypeEnum.SpeedTrain:
85                     CurrencyEnumChinese = "高铁";
86                     break;
87                 case TrafficTypeEnum.AirPlane:
88                     CurrencyEnumChinese = "飞机";
89                     break;
90                 case TrafficTypeEnum.Ship:
91                     CurrencyEnumChinese = "轮船";
92                     break;
93                 default:
94                     break;
95             }
96             return CurrencyEnumChinese;
97         }
98     }
99 }

 

 基类和子类可以存放在同一个数据集里面

 1.注意 _t 字段,这里保存着基类的类型,如果是子类的话,这个字段是空的。

2.编码的时候请注意,子类和基类 保存/序列化 的时候不会出现什么问题,但是读取/反序列化的时候,一定会出现错误。

请在基类上增加这样的特性标签

1  [BsonKnownTypes(typeof(Traffic), typeof(Visit), typeof(Shopping), typeof(Catering))]

这样的话,实际上是为系统注册了基类和子类关系,就可以正确的反序列化了。

 

我们是否要将一个巨大的对象保存为一个数据文档(数据文档在MongoDB里面是一条记录的意思)

一个巨大的对象,往往是一个树型结构的数据。

例如,一个旅行行程表,它包含了一个行程概要,一个礼物列表,一个[每日行程列表],一个事前准备列表。

每一个  [每日行程列表] 又包含了 【详细行程】列表,【详细行程】中又包含了【消费明细】等等。

 

 

这样的话,一个行程就是一个巨大的对象(巨大的树形结构数据),当然,MongoDB是支持将整棵树作为一个数据文档,一下子保存到数据库中去的。

(每一个数据文档的大小是有限制的,但是那个限制也是一个天文数字,我想没有人会把整部三国演义作为一个对象放到数据库里面去的吧)

不过,这样对于数据库的管理将是一个巨大的灾难,这个灾难不仅仅是在数据进行更新时候,更新命令将会变得很复杂,而且会在各种聚合操作的时候,将会遍历所有的数据对象。

 这里使用了一种折衷的办法,将一个3层或者4层的结构,拆分为两个数据表,一个里面包含上面两层数据,然后做一个摘要结构,包含着下两层信息的一个简报。一些简单的信息直接从简版里面读取。

下两层则放置在另一个数据库中。只有在需要详细数据的时候才读取出来呈现在界面上。

 

这里的设计也就是一个数据存储的粒度的设计,粒度太小,就退化为关系型数据库。反之则每次更新将涉及整棵文档树。粒度设计是MongoDB的一个难点。

 

2.将MongoDB作为文件服务器使用

MongoDB的Grid File System特性就是一个内置的文件存储空间。你可以像管理数据一样管理文件。

对于文件的读取,其实很简单

C#驱动的Download和Upload方法提供了读取和保存文件的方法

 1         /// <summary>
 2         /// 保存文件
 3         /// </summary>
 4         /// <param name="file"></param>
 5         /// <param name="Username"></param>
 6         /// <returns></returns>
 7         public static string InsertFile(HttpPostedFile file, string Username)
 8         {
 9             string Mongofilename = Username + "_" + System.DateTime.Now.ToString("yyyyMMddHHmmss") + "_" + file.FileName;
10             MongoGridFS gfs = innerFileServer.GetGridFS(new MongoGridFSSettings());
11             gfs.Upload(file.InputStream, Mongofilename);
12             return Mongofilename;
13         }
14         /// <summary>
15         /// 获得文件
16         /// </summary>
17         /// <param name="stream"></param>
18         /// <param name="filename"></param>
19         public static void GetFile(Stream stream,string filename)
20         {
21             MongoGridFS gfs = innerFileServer.GetGridFS(new MongoGridFSSettings());
22             gfs.Download(stream,filename);
23         }

 

当然,我们想做的事情是让IIS请求图片的时候,不走静态文件这条路,而是从数据库里面读取文件。

这里我们要自定义 http Handler

a.修改 webconfig

注意,经典模式和集成模式是不同的

我们让系统对于图片文件,用我们自定义的方法进行处理,JPG,GIF,PNG的响应使用ImageServer类来处理

  <system.webServer>
    <handlers>
      <add name="ImageServerJPG" path="*.jpg" verb="GET" type="TaiWanTripPlanSite.ImageServer" />
      <add name="ImageServerGIF" path="*.gif" verb="GET" type="TaiWanTripPlanSite.ImageServer" />
      <add name="ImageServerPNG" path="*.png" verb="GET" type="TaiWanTripPlanSite.ImageServer" />
    </handlers>
  </system.webServer>

b.ImageServer

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;

namespace TaiWanTripPlanSite
{
    public class ImageServer : IHttpHandler
    {
        public bool IsReusable
        {
            get { return true; }
        }

        public void ProcessRequest(HttpContext context)
        {
            var url = context.Request.Url.LocalPath.Split("/".ToCharArray());
            var filename = url.Last();
            ///设置为最大时间,图片都是统一的文件名字,不会重复
            context.Response.Expires = int.MaxValue;
            Common.Database.Operater.GetFile(context.Response.OutputStream, filename);
        }
    }
}

这里需要注意的是,一定要设定过期时间,不然的话,浏览器将不能使用缓存图片,每次访问同样的资源将进行全量下载。

这里将过期时间设定为最大值,图片资源将永不过期。

 

总结

这篇文章只是提供了一个使用MongoDB作为数据库和文件服务的方案,对于性能没有任何的测试。

阶层型数据库需要在粒度上进行总体的规划和设计。对于MongoDB,可以使用内置的文件服务功能,他对于同名文件,支持版本管理功能。

如果有人愿意,可以进行一个压力测试,看一下MongoDB的性能如何。

MongoDB的强大在于分布式,分片,副本等高级功能。这里只是抛砖引玉。

欢迎和大家一起学习MongoDB,讨论MongoDB

(本人已经接受英国某出版社的委托,作为志愿者Review MongoDB的视频课程)

本文网站: 笔者花了一个星期的作品,使用UIKit组件,webform,mongodb数据库,仅供参考,让前端大牛见笑了。

www.meishiyouji.com

 

广告时间:

如果你对我的技术认可,并且有志于从事前端开发,愿意和我一起共事,能否投个简历到  上海中和 软件公司

Email:   [email protected]

或者在 拉钩网 搜索   中和软件 的职位。

http://www.lagou.com/gongsi/37348.html

欢迎有2-5年前端经验的人应聘,我将会亲自带你们学习知识,提高技能,让你们实现自我价值。

郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。