使用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数据库,仅供参考,让前端大牛见笑了。
广告时间:
如果你对我的技术认可,并且有志于从事前端开发,愿意和我一起共事,能否投个简历到 上海中和 软件公司。
Email: mynightelfplayer@hotmail.com
或者在 拉钩网 搜索 中和软件 的职位。
http://www.lagou.com/gongsi/37348.html
欢迎有2-5年前端经验的人应聘,我将会亲自带你们学习知识,提高技能,让你们实现自我价值。