使用LinqToSql加载动态column
需求
最近有个小的探索类需求:如何支持在数据库中动态的添加一列?
例如我们有一个type News
news
如果客户说希望多一个属性叫ExpireAt怎么办?
方案
想了想,数据库schema方面大概有如下几种办法:
- Sharepoint那样的schema,把表定义和rows分离开。确实灵活性很大,但是数据可读性比较差,而且复杂度也高
- 给news表创建一列叫个什么ExtendedProperties之类的,专门用于存储键值对,虽然很简单但是bad design。
- 在维持news表不动的情况下创建sharepoint式的schema表和values表,用于存储扩展属性的schema和其值
- 在news表上真的创建新的column ExpireAt
本文来实现方案4
技术选型
- 那么具体到方案4,技术上又有很多选择,从orm工具来说
- 不用orm,这个当然可以。。。
- linq to sql,可以通过使用DataContext.GetTable(Type)方法得到ITable对象,type可以运行时决定,所以满足需求
- NHibernate,可以使用dynamic-component,维持Domain model不变的情况下只修改XXXX.hbm.xml完成
当然news的定义中需要加上一个Dictionary用于存储“dynamic-component”
这个方案的问题在于想要使修改过的XXXX.hbm.xml(可以使用序列化或者新的ConfORM)起作用必须重新build SessionFactory(据我所知如此),不知道如何在不影响现有Sessions的情况下做到
- 其他,不熟。。。没准儿Entity Framework的Code First模式可以?
思路
原计划是在已经加好Table、Column之类Attribute的News基础上运行是动态创建出来一个NewsXXXXX继承于News,添加一个property ExpireAt,加上ColumnAttribute了事。可是实现中发现Linq to Sql不支持这种继承。于是计划变更为只定义POCO的News,需要传入GetTable的时候做一个Wrap的News出来,添加上Table,Column之类的Attribute。当用户创建了新的property,从News中继承出新的POCO的News,然后重新Wrap。最终我的实现大体如下。
news
当然上面只是个示意,后三个类实际上在我的代码中是不存在的,都是运行时创建,名字也是随机的,大家理解个意思就成。。。
注意原始的News类里面属性都要是virtual的了,因为我要在运行时继承它们,这一点上变得像NHibernate了不太爽。。。不过好在在上层的代码之中我只会用到INews而不会用到News,这个后面马上会谈到。
模型设计
直接上图
为了屏蔽不同orm之间实现细节,我需要上层只能见到接口见不到实现。需要new的时候就去找IEntityFactory。
其中IContentEntity是所有可以拥有扩展属性的实体类需要实现的接口,例如本例中的INews。上层访问其property的时候就可以调用其indexer,而不关心这个property是真正的class中的property还是从字典中而来(例如NHibernate)。
IProperty自然就是允许用户创建的Property,本例中Property创建成功后会触发一个Added事件,监听方就会继承出XXXNews和WrappedXXXNews。
IView是控制界面上显示哪些property用的,在我的实现中http://site/news/ 只会显示INews中定义的三个property(不包含indexer),而如果访问http://site/news/views/viewId 就会根据viewId来决定显示哪些property。
IListView更进一步的提供了filter的能力。其实filter应该做为另一个单独的接口才是的。。。
技术细节
EntityFactory的实现
代码如下
entity factory
各个entity(除了contentEntity)都在自己的type initializer里注册自己的constructor,例如
type initialier
但是content entity不同,是在wrap之后注册的,因为Wrap过的type里才包含orm需要的table、column这些attribute,代码如下
wrap
动态类型生成
本例中采用了两种动态生成类型的方式。
News –> XXXNews这个过程使用了Emit
News –> WrappedNews这个过程使用了codeDom
关于前者,有一点需要说明:
由于XXXNews以后还需要进一步包装为WrappedXXXNews,所以Emit出来的Assembly需要Save到硬盘。存在什么位置呢?我曾经使用了缺省值(Environment.CurrentDirectory),但是XXXNews –> WrappedXXXNews时报告说找不到Emit出来的Assembly(这个异常好隐蔽。。。花了我很久。。。)。于是我尝试Save到bin目录下。但是对bin目录内容的更改又会造成ASP.NET的重编译(听说。具体发生了什么求赐教),于是存到了bin2。。。我也知道这是个很雷的方案。。。在web.config中设置
<probing privatePath="bin;bin2"/>
就可以了。
关于后者,我使用了Expressions to CodeDOM,使用过程实在算不上顺手。。。求推荐更好的codedom类库。
Controller的依赖注入
园子里的刘东大人(spring.net达人)已经介绍过了使用spring.net 1.3.1对controller的注入。
我在这里说两个问题
- 好像还是要依赖Spring.Core的,估计刘东大把这个assembly加入了GAC?
- 还是不知道怎么兑Global.asax进行注入。目前只会比较丑陋的写法
ContextRegistry.GetContext().GetObject("someObj"),而且只能在Application_Start之后使用
接下来的路
本例中还有很多未尽事宜,例如
LambdaExpressionBinder没有完成,所以create一个list view的时候无法把用户输入的lambda表达式转为Func。这个相信用codedom也能做,只是我实在懒得弄了。
还没有实现允许用户输入ExpireAt,也懒得弄了。。。
Wrap content entity type的时候,本来应该读取对应的property,加上相应的column参数的,懒得弄了。。。
还有很多基本的地方,例如NewsProperty、NewsListView根本都没有mapping到数据库。。。。。。懒得弄了。。。
等等等等
你有多懒啊!喂!
不过有些地方不是我懒哦
比如不知道Spring.Net的ControllerFactory是怎么实现的(以后抽个时间看看),我必须要写以下三个Controller,直接在配置文件中指定泛型Controller不行。以后直接上Mvc3试试好了。
NewsPropertyController
NewsListViewController
NewsController
虽然如上所述,本例还有非常多的不足,不过我不会再更新了(这不是坑。。。
本来就是个技术学习性质的东西,实际应用可能性很小(比如现在大家都不会用Linq to Sql了吧)。我也真的在这个过程中学到了不少东西,这对我就足够了。
代码下载与声明
本例使用Spring.Net实现IoC和singleton
本例使用了Expressions to CodeDOM实现codedom
本例中的TypeBinder基本照抄自《Pro ASP.NET MVC 2 Framework 2nd Edition》中的实现
运行本例前请自备数据库结构如下(就一个表)
并自行修改web.config中的连接串
按F5 首先出现news list
然后请进news properties
之后Add我们需要的ExpireAt
Save成功后再次进入news,这个list不会变化,尝试进入view,就会看到ExpireAt了
最后给出本例下载