YbSoftwareFactory 代码生成插件【十九】:实体类配合数据库表字段进行属性扩展的小技巧
实体类通常需要和数据库表进行了ORM映射,当你需要添加新的属性时,往往同时也需要在数据库中添加相应的字段并配置好映射关系,同时可能还需对数据访问组件进行重新编译和部署才能有效。而当你开始设计一个通用数据访问组件后,因为项目需求的不同和需求的不断变化演变,很难不能保证不会再添加额外的属性和字段。特别是项目部署运行后,添加一个属性和字段带来的额外维护的工作量可能要远远超过对代码进行调整的工作量。本文提供了属性字段扩展的一种思路,在满足核心字段可通过实体类强类型进行访问的同时,还可通过C# 4.0提供的dynamic特性和Dictionary等技术手段进行字段、属性的扩展,并对数据访问的统一封装,具有通用性强、使用方便、扩展能力强等优点。
本文用到了前面提到的ExtensionObject,其是进行属性扩展原理的核心类,该类继承自DynamicObject类,并实现了, IDynamicMetaObjectProvider,IDictionary<string,object>等接口。和.NET Framework中ExpandoObject类不同的是,继承自DynamicObject的类可以添加实例属性,而ExpandoObject因为被设计为“sealed”类,因此它只能在运行时动态添加属性;另外,继承自DynamicObject的类可实现自定义的对其成员进行管理的一系列方法,因此和ExpandoObject类相比,从DynamicObject类继承无疑具有更高的灵活性。对ExtensionObject类的实现不清楚的可先看看前面的文章:http://www.cnblogs.com/gyche/p/3223341.html。
在YbSoftwareFactory的一些底层数据访问组件中,例如ConcreteData字典、HierarchyData字典、组织机构实体类、权限实体类、用户信息实体类、角色定义实体类等均已继承自ExtensionObject并实现了对应的对扩展的字段进行数据访问和管理的方法,从实际的运用效果来看,在字段、属性的扩展上确实是非常的灵活和方便。
动态属性扩展的步骤如下:
1、首先,通过让实体类继承自“ExtensionObject”,因为ExtensionObject继承自DynamicObject,并实现了IDictionary<string,object>和索引器,这样实体类就具有了动态属性的自管理功能,在通过强类型访问其实例属性的同时,也能通过dynamic,IDictionary接口和索引器访问其实例属性和动态属性。
例如定义一个用户类并添加必要的实例属性如下:
[Serializable]
public class User : ExtensionObject
{
public Guid UserId { get; set; }
public string Email { get; set; }
public string Password { get; set; }
public string Name { get; set; }
public bool Active { get; set; }
public DateTime? ExpiresOn { get; set; } public User() : base()
{ } public User(object instance) : base(instance)
{
}
}
然后,就可通过如下方式进行实例属性和动态属性的访问,是不是非常灵活和方便:
var user = new User();
// 通过实例属性进行访问
user.UserId = Guid.NewGuid();
user.Password = "YbSofteareFactory";
//通过动态方式进行实例属性的访问
dynamic duser = user;
duser.Email = "19892257@qq.com";
// 追加动态属性
duser.FriendUserName = "YB";
duser.CreatedDate = DateTime.Now;
duser.TodayNewsCount = ;
duser.Age = 27.5;
duser.LastUpdateId = (Guid?)null;
duser.LastUpdatedDate = null;
//通过索引器也可进行实例属性和动态属性的访问和追加
user["LastUpdatedDate"] = DateTime.Now;
2、实现对扩展字段的数据库访问:
1 #region 加载扩展属性
/// <summary>
/// 为指定的ConcreteData集合加载扩展属性
/// </summary>
/// <param name="items">待加载的ConcreteData集合</param>
public override void LoadExtPropertiesFor(IEnumerable<ConcreteData> items)
{
//判断是否需要加载
//_extFields为需加载的字段名称字符串,如“NewField1,NewField2”,通过config文件进行配置。
if (_extFields.Length > && items.Any() )
{
//转换为字典,方便后续进行处理
var dic = items.ToDictionary(c => c.ConcreteDataId);
//组合标识字符串
var ids = string.Format("'{0}'",string.Join("','",dic.Keys.ToArray()));
using (HostingEnvironment.Impersonate())
using (var db = this.connectionStringSetting.CreateDbConnection())
using (var cmd =
this.CreateDbCommand(string.Format("SELECT ConcreteDataId,{0} FROM $TableName WHERE ConcreteDataId IN ({1})",_extFields,ids), db))
{
cmd.AddParameterWithValue("@ids", ids);
db.Open();
using (var r = cmd.ExecuteReader())
{
while (r.Read())
{
//获取标识
var concreteDataId = r["ConcreteDataId"] as string;
//根据字典获取待加载动态属性值的实体
var item = dic[concreteDataId];
foreach (var extField in _extFieldArr)
{
var value = r[extField];
//通过ExtensionObject类的索引器设置动态属性及相应的值
item[extField] = value != DBNull.Value ?value:null;
}
}
}
}
}
}
#endregion
#region 保存扩展属性
public override void SaveExtPropertiesFor(IEnumerable<ConcreteData> items)
{
if (_extFields.Length > )
{
//获取待更新扩展属性的SQL更新语句
var updateSql = string.Join(",", _extFieldArr.Select(c => string.Format("{0} = @{0}", c)));
using (HostingEnvironment.Impersonate())
using (var db = this.connectionStringSetting.CreateDbConnection())
using (
var cmd =
this.CreateDbCommand(
string.Format("UPDATE $TableName SET {0} WHERE ConcreteDataId = @ConcreteDataId",
updateSql), db))
{
db.Open();
DbTransaction sqlTransaction = db.BeginTransaction();
cmd.Transaction = sqlTransaction;
try
{
foreach (var item in items)
{
cmd.Parameters.Clear();
cmd.AddParameterWithValue("@ConcreteDataId",item.ConcreteDataId);
foreach (var extField in _extFieldArr)
{
if (item.Contains(extField, true) && item[extField] != null)
{
//如果实体的属性包含配置的字段名,则追加更新参数及值
cmd.AddParameterWithValue(string.Format("@{0}", extField),
item[extField]);
}
else
{
//如果实体的属性不包含配置的字段名,则取消对该字段的更新
cmd.CommandText = cmd.CommandText.Replace(string.Format("@{0}", extField), "NULL");
}
}
cmd.ExecuteNonQuery();
}
//进行事务提交
sqlTransaction.Commit();
}
catch (Exception)
{
sqlTransaction.Rollback();
throw;
}
}
}
}
#endregion
3、为了更方便使用,我们在此处可进一步封装,我们可在所配置的Provider初始化时把config文件中配置好的extFields读出来即可,如下是初始化方法的实现:
#region Initialize
public override void Initialize(string name, System.Collections.Specialized.NameValueCollection config)
{
// Validate arguments
if (config == null) throw new ArgumentNullException("config");
if (string.IsNullOrEmpty(name)) name = "YbConcreteDataProvider";
if (String.IsNullOrEmpty(config["description"]))
{
config.Remove("description");
config.Add("description", "Yb concrete data provider");
}
//判断是否存在tableName属性
if (String.IsNullOrEmpty(config["tableName"]))
{
config.Remove("tableName");
//添加默认的表名
config.Add("tableName", "YbConcreteData");
}
//判断是否存在extFields属性
if (string.IsNullOrEmpty(config["extFields"]))
{
config.Remove("extFields");
//不存在则可设置为"",这样将不会对任何扩展的新字段进行访问
config.Add("extFields", "");
}
// Initialize base class
base.Initialize(name, config);
// Read connection string
this.ConnectionStringName = config.GetConfigValue("connectionStringName", null);
if (string.IsNullOrWhiteSpace(this.ConnectionStringName))
throw new ConfigurationErrorsException(Resources.Required_connectionStringName_attribute_not_specified);
this.connectionStringSetting = ConfigurationManager.ConnectionStrings[this.ConnectionStringName];
if (this.connectionStringSetting == null)
throw new ConfigurationErrorsException(string.Format(Resources.Format_connection_string_was_not_found,
this.ConnectionStringName));
if (string.IsNullOrEmpty(this.connectionStringSetting.ProviderName))
throw new ConfigurationErrorsException(
string.Format(
Resources.Format_connection_string_does_not_have_specified_the_providerName_attribute,
this.ConnectionStringName));
//激发设置连接字符串前的事件处理程序,主要目的是解密连接字符串
ConnectionStringChangingEventArgs args =
RaiseConnectionStringChangingEvent(connectionStringSetting.ConnectionString);
if (args == null) throw new ProviderException(Resources.Connection_string_cannot_be_blank);
if (!this.connectionStringSetting.ConnectionString.Equals(args.ConnectionString))
{
this.connectionStringSetting =
new ConnectionStringSettings(this.ConnectionStringName, args.ConnectionString,
this.connectionStringSetting.ProviderName);
}
if (string.IsNullOrWhiteSpace(connectionStringSetting.ConnectionString))
throw new ProviderException(Resources.Connection_string_cannot_be_blank);
this.applicationName = config["applicationName"];
//获取配置文件中配置的数据库实际表名
this.tableName = config["tableName"];
SecUtility.CheckParameter(ref tableName, true, true, true, , "tableName");
//获取配置文件中配置的新扩展的字段名集合
_extFields = config.Get("extFields").Trim();
if (!string.IsNullOrEmpty(_extFields))
{
//进行字符串分割,转换为字段数组,方便后续的处理
_extFieldArr = _extFields.Split(new[] {','}, StringSplitOptions.RemoveEmptyEntries);
_extFieldArr = _extFieldArr.Select(c => c.Trim()).ToArray();
}
}
#endregion
最后看看单元测试代码可进一步理解其调用的具体过程,在数据库中扩展的字段名仅需在config配置文件中设置即可生效,同时在调用方式上进行了统一,最终无需传递扩展的字段名称、类型等参数,在实体对象中也能获取和设置这些新添加的属性的值。单元测试代码如下(此处扩展了三个字段:“NewField1”,“NewField2”,“NewField3”,类型分别为string,bool,DateTime):
/// <summary>
///UpdateConcreteData 的测试
///</summary>
[TestMethod()]
public void ConcreteData_UpdateConcreteDataTest()
{
ConcreteData concreteData = MyConcreteData; // TODO: 初始化为适当的值
bool expected = true; // TODO: 初始化为适当的值
bool actual;
actual = ConcreteDataApi.UpdateConcreteData(concreteData);
Assert.AreEqual(expected, actual); //保存扩展属性值为null
ConcreteDataApi.SaveExtPropertiesFor(concreteData);
ConcreteDataApi.LoadExtPropertiesFor(concreteData);
Assert.IsNull(concreteData["NewField1"]);
Assert.IsNull(concreteData["NewField2"]);
Assert.IsNull(concreteData["NewField3"]);
//设置扩展属性值
concreteData["NewField1"]="";
concreteData["NewField2"] = true;
concreteData["NewField3"] = DateTime.Now;
ConcreteDataApi.SaveExtPropertiesFor(concreteData);
var item = ConcreteDataApi.GetConcreteDataWithExtProperties(concreteData.ConcreteDataId);
Assert.AreEqual(item["NewField1"],"");
Assert.AreEqual(item["NewField2"],true);
Assert.IsNotNull(item["NewField3"]); concreteData["NewField1"] = "";
concreteData["NewField2"] = null;
concreteData["NewField3"] = null;
ConcreteDataApi.SaveExtPropertiesFor(concreteData);
item = ConcreteDataApi.GetConcreteDataWithExtProperties(concreteData.ConcreteDataId);
Assert.IsNull(item["NewField1"]);
Assert.IsNull(item["NewField2"]);
Assert.IsNull(item["NewField3"]);
}
通过上述设计,确保了每个数据访问组件默认情况下只需加载必要的字段(即实体类的实例属性),并预留了对新扩展字段的数据访问接口,在提高了灵活性和可扩展性的同时,还兼顾了性能方面的考虑。
下一章将介绍对扩展自ExtensionObject的对象进行Json序列化的具体实现,这样就可让ExtensionObject和MVC实现完美的集成,而无需再进行中间层次的模型转换。
附三:权限模型Demo
YbSoftwareFactory 代码生成插件【十九】:实体类配合数据库表字段进行属性扩展的小技巧的更多相关文章
- YbSoftwareFactory 代码生成插件【二十】:DynamicObject的序列化
DynamicObject 是 .NET 4.0以来才支持的一个类,但该类在.NET 4.0下未被标记为[Serializable] Attribute,而在.NET 4.5下则被标记了[Serial ...
- YbSoftwareFactory 代码生成插件【十四】:通过 DynamicLinq 简单实现 N-Tier 部署下的服务端数据库通用分页
YbSoftwareFactory 的 YbRapidSolution for WinForm 插件使用CSLA.NET作为业务层,CSLA.NET的一个强大的特性是支持 N-Tiers 部署.只需非 ...
- 在Code First中使用Migrations对实体类和数据库做出变更
在Code First中使用Migrations对实体类和数据库做出变更,Mirgration包含一系列命令. 工具--库程序包管理器--程序包管理器控制台 运行命令:Enable-Migration ...
- 解决mybatis实体类和数据库列名不匹配的两种办法
我们在实际开发中,会遇到实体类与数据库类不匹配的情况,在开发中就会产生各种各样的错误,那么我们应该怎么去解决这一类的错误呢?很简单,下面我们介绍两种解决方法: 首先我们看一下数据库和实体类不匹配的情况 ...
- ASP.NET Core EFCore 之DBFirst 自动创建实体类和数据库上下文
通过引用Nuget包添加实体类 运行 Install-Package Microsoft.EntityFrameworkCore.SqlServer 运行 Install-Package Micros ...
- 关于解决SpringDataJpa框架实体类表字段创建顺序与数据库表字段展示顺序不一致的问题
今天在公司的项目开发中,遇到一个问题: 后端对象实体类中写入字段顺序与数据库中的存储顺序不一致. 仔细观察到数据库中的表字段的排序方式是按照拼音字母的顺序abcdef......来存储的 而我的实体类 ...
- YbSoftwareFactory 代码生成插件【十五】:Show 一下最新的动态属性扩展功能与键值生成器功能
YbSoftwareFactory 各种插件的基础类库中又新增了两个方便易用的功能:动态属性扩展与键值生成器,本章将分别介绍这两个非常方便的组件. 一.动态属性扩展 在实际的开发过程中,你肯定会遇到数 ...
- YbSoftwareFactory 代码生成插件【二十二】:CMS基础功能的实现
很多网友建议在YbRapidSolution for MVC框架的基础上实现CMS功能,以方便进行内容的管理,加快前端页面的开发速度.因此花了一段时间,实现了一套CMS内容发布系统并已集成至YbRap ...
- Eclipse使用hibernate插件反向生成实体类和映射文件
一般dao层的开发是这样的,先进行数据库的设计,什么E-R图之类的那些,然后选择一款数据库产品,建好表.最后反向生成Java实体和映射文件,这样可以保证一致性和便捷性. 如果用myeclipse,逆向 ...
随机推荐
- WAF攻防研究之四个层次Bypass WAF
从架构.资源.协议和规则4个层次研究绕过WAF的技术,助于全方位提升WAF防御能力. 绕过WAF的相关技术研究是WAF攻防研究非常重要的一部分,也是最有趣的部分,所以我在写WAF攻防时先写攻击部分.还 ...
- Spring中Bean的作用域
1.在Spring的早期版本中,仅有两个作用域:singleton和prototype,前者表示Bean以单例的方式存在:后者表示每次从容器中调用Bean时,都会返回一个新的实例 2.Spring 2 ...
- 【完整靠谱版】结合公司项目,仔细总结自己使用百度编辑器实现FTP上传的完整过程
说在前面 工作中会遇到很多需要使用富文本编辑器的地方,比如我现在发布这篇文章离不开这个神器,而且现在网上编辑器太多了.记得之前,由于工作需要自己封装过一个编辑器的公共插件,是用ckeditor改版的, ...
- 史上最全的java随机数生成算法分享(转)
这篇文章主要介绍了史上最全的java随机数生成算法,我分享一个最全的随机数的生成算法,最代码的找回密码的随机数就是用的这个方法 String password = RandomUtil.generat ...
- Spring的IOC原理(转载)
在网上看到一篇文章,感觉写得挺不错的,转载一下,本文转载自:http://www.cnblogs.com/xdp-gacl/p/3707631.html 一. IoC理论的背景 我们都知道,在采用面向 ...
- 响应链和UIKit框架
Event Delivery: The Responder Chain When you design your app, it’s likely that you want to respond t ...
- node静态资源管理变迁之路
使用express自带的,express.static,如:app.use(express.static('hehe')),就可以用localhost/hua.png,访问项目根目录下,hehe文件夹 ...
- 临时存存储页面上的数据---Web存储
HTML5 Web存储的两种方法使用 localStorage和sessionStorage 参考: http://www.cnblogs.com/taoweiji/archive/2012/12/0 ...
- JMeter学习-032-JMeter常见四种变量简介
在JMeter自动化测试脚本编写过程中,经常需要对测试脚本进行一些参数设置.例如,设置测试计划的全局变量(方便切换不同的测试环境).样本线程(HTTP请求等)的参数传递等. 通常,JMeter中常用的 ...
- Centos minimal安装
Q:为什么要安装minimal版本?而不是完整版 A:因为它资源小啊 @ @ 怎么安装就不说了,网上资源大把,Centos7好像变化挺大,mysql都装不上,还是装6吧~ 1.安装完是不能联网的,解决 ...