数据库驱动的业务层模板
介绍使用 SQL 扩展属性在业务层代码模板中实现的灵活性。

目录
引言
看到“生成成功”总是令人高兴,但看到在几秒钟内添加了数千行新代码后“生成成功”则是一件美妙的事情。代码生成在过去几年里为我节省了无数工作时间,但也带来了一些麻烦。我使用过一些生成需要后期修改才能编译的代码的模板。我使用的其他模板会生成完整的类,其中“预留空间”用于自定义代码,因此在重新生成对象时必须小心不要覆盖自定义代码。总的来说,我可以直接运行模板、编译并完成工作的次数屈指可数。
因此,我当前项目业务层模板的头号目标是实现“一键式代码生成”。换句话说,我想要运行一个程序,根据当前数据库状态重新生成所有业务实体,不影响任何自定义代码,也不会破坏任何东西,除了数据库模式变更的结果。此外,我希望业务实体简洁直观,以便我的团队成员能够立即上手编码,而无需我的任何帮助。
我最终使用了名为 MyGeneration 的代码生成工具,并结合了 EntitySpaces 业务层框架(和模板)。本文讨论的问题并非 EntitySpaces 特有,但我自定义并正在使用的模板是 EntitySpaces 模板。EntitySpaces 模板与 MyGeneration 结合开箱即用地解决了“一键式”需求。MyGeneration 允许您保存之前运行的设置并将其输入到模板中,从而实现一键式重新生成业务层。此外,EntitySpaces 模板使用部分类为生成代码和自定义代码创建单独的文件,因此您永远不必担心在重新生成业务实体时覆盖自定义代码。如果您使用的是不使用部分类和多个文件的模板,您真的需要寻找其他东西。生成的代码不应与自定义代码放在同一个文件中——这会导致不必要的麻烦。
在满足了第一个要求后,我花大部分时间修改模板以适应个人偏好和功能需求。我希望为某些表生成枚举,能够将生成的文件组织到子文件夹中,以便在 Visual Studio 中更容易浏览,以及一种更改某些外键引用属性名称的方法,但最重要的是,我想让类名不再是表名。我更喜欢复数表名("Select * From User"
听起来不像是在选择多个用户),但类名应该是单数("Users u = new Users()"
听起来不像代表一个用户)。存储表到类名映射的最合乎逻辑的地方是数据库,因为那是控制代码生成的。我最初的想法是创建自定义表来存储关于表和列的信息,但后来我发现 SQL Server 已经实现了这一点。你好,扩展属性!
SQL 扩展属性入门
管理数据库中扩展属性的最简单方法是在 Management Studio 的对象资源管理器中右键单击数据库、表或列,选择“属性”,然后在“属性”对话框中选择“扩展属性”。此界面允许您轻松地为单个对象添加和更新扩展属性值。如果您想以编程方式操作属性值,可以通过几个系统存储过程和函数来访问它们。
sp_addextendedproperty
@name, @value,
@level0type, @level0name,
@level1type, @level1name,
@level2type, @level2name
sp_updateextendedproperty
@name, @value,
@level0type, @level0name,
@level1type, @level1name,
@level2type, @level2name
sp_dropextendedproperty
@name,
@level0type, @level0name,
@level1type, @level1name,
@level2type, @level2name
fn_listextendedproperty
@name,
@level0type, @level0name,
@level1type, @level1name,
@level2type, @level2name
要向表添加扩展属性,请使用以下参数
exec sp_addextendedproperty
'[PropertyName]',
'[PropertyValue]',
'user',
'dbo',
'table',
'[TableName]',
null,
null
要向列添加扩展属性,请使用以下参数
exec sp_addextendedproperty
'[PropertyName]',
'[PropertyValue]',
'user',
'dbo',
'table',
'[TableName]',
'column',
'[ColumnName]'
fn_listextendedproperty
返回一个表(“objtype”、“objname”、“name”、“value”),因此可以多种方式使用。您可以传入表、列和属性的名称来获取该属性的值,也可以在此处或那里留空,在大多数情况下,它们充当通配符。以下是一些示例
-- Get all table-level extended properties in the database
Select * From
fn_listextendedproperty (null, 'user', 'dbo', 'table', null, null, null)
-- Get all column-level extended properties for the Employees table
Select * From
fn_listextendedproperty (null, 'user', 'dbo', 'table', 'employees', 'column', null)
-- Get the value of the "Biz.Class" property for every table in the database
Select objname, value From
fn_listextendedproperty ('Biz.Class', 'user', 'dbo', 'table', null, null, null)
-- Get all extended properties defined on the database itself
Select * From
fn_listextendedproperty (null, null, null, null, null, null, null)
更新过程与添加过程完全相同。删除过程语法唯一的区别在于,它似乎不允许为属性、表或列名称设置 null 值,因此您一次只能删除一个属性。显然,微软希望您非常具体地指明要删除的内容。
整合起来
既然您知道如何使用扩展属性,是时候将它们整合起来了。下一步是修改您的代码生成模板,使它们能够针对您的数据库运行上述查询,检索属性值,并根据这些值生成代码。您可以根据业务层需求,在您选择实现的属性方面采取多种方向。我决定坚持以下属性。您可以在随附的 NorthwindExtendedProperties.csv 文件中找到每个表的精确值。
Biz.Class
- 此表的类名;通常是复数表名的单数形式。Biz.Path
- 用于创建文件的子路径,按应用程序/数据库功能组织。您也可以将其用于命名空间,但 EntitySpaces 要求所有实体都位于同一命名空间中,因此我没有更改命名空间的生成。我将 Northwind 组织为“Customers”、“Employees”、“Orders”和“Products”组。Biz.Enum (可选)
- 基于此表数据生成的枚举名称。Biz.Enum.NameColumn
- 其值代表枚举值名称的列名。Biz.Enum.ValueColumn
- 其值代表数值枚举值的列名。
修改模板本身相当简单。我创建了一个方法来加载给定表或视图的扩展属性并缓存它们,然后将它们作为 KeyValueCollection
返回。在模板的其他地方,我用我自己的方法替换了 EntitySpaces 生成对象和属性名称的自定义方法,这些方法使用缓存的属性值。下面您可以看到用于加载扩展属性值的函数,以及一个使用这些值的函数的示例。我相信 DatabaseSpecific
类是 EntitySpaces 帮助库的一部分,但您也可以很容易地将其替换为调用 fn_listextendedproperty 的 SqlCommand。
System.Collections.Generic.Dictionary<object,KeyValueCollection> props =
new System.Collections.Generic.Dictionary<object,KeyValueCollection>();
private KeyValueCollection GetExtendedProperties(ITable tbl,IView vw)
{
object key = ((object)tbl) ?? ((object)vw);
if( !props.ContainsKey(key) )
{
MyMeta.Sql.DatabaseSpecific dbs =
new MyMeta.Sql.DatabaseSpecific();
KeyValueCollection kvc;
if( tbl != null )
kvc = dbs.ExtendedProperties(tbl);
else
kvc = dbs.ExtendedProperties(vw);
props.Add(key,kvc);
}
return props[key];
}
private string EntityName(ITable tbl, IView vw)
{
KeyValueCollection kvc = GetExtendedProperties(tbl,vw);
foreach(KeyValuePair kvp in kvc)
if(kvp.Key == "Biz.Class")
return kvp.Value;
return "MissingEntityName";
}
private string EntityPath(ITable tbl,IView vw)
{
string path = "";
KeyValueCollection kvc = GetExtendedProperties(tbl,vw);
foreach(KeyValuePair kvp in kvc)
if(kvp.Key == "Biz.Path")
path = kvp.Value;
return path.Replace(".","");
}
下面是一些我如何修改模板以在生成的代码中使用扩展属性值的示例。
public partial class <%=EntityName(table,view)%> : <%=esEntityName(table,view)%>
// was formerly
public partial class <%=esPlugIn.Entity(source)%> : <%=esPlugIn.esEntity(source)%>
string objName = EntityName(tr.ForeignTable, null) + "By" + tr.PrimaryColumns[0].Name;
// was formerly
string objName = esPlugIn.EntityRelationName(tr.ForeignTable, tr.PrimaryColumns[0], tr.IsSelfReference);
// Modification to write business entity files to sub-folders based on Biz.Path setting
string filename = input["txtPath"].ToString();
if (!filename.EndsWith("\\") )
filename += "\\";
if(!String.IsNullOrEmpty(EntityPath(table,view)))
filename += EntityPath(table,view);
我还对 EntitySpaces 模板进行了一些其他小的调整以满足我的需求,例如从引用父行的外键属性中删除“UpTo”前缀,但实际上我不建议这样做,如果您是 EntitySpaces 用户。虽然“UpTo”前缀可以使 intellisense/auto-complete 稍微不那么有用(键入“OrdersBy”不会找到 UpToOrdersByCustomerID
属性),但它有助于提醒关系的方向,并且对于自引用表是必需的。
从表中构建枚举
当需要表示由一组固定选项组成的数据类型时,例如 Northwind 中的 Categories
或 Region
表,人们倾向于仅使用单独的数据库表(通过外键链接)或普通数字列和代码中的枚举来解释它。我说为什么不两者兼顾?有时您需要数据库表的功能来表示名称和值之外的其他信息,但您也希望能够访问集合中的特定值(例如:Project.Status = ProjectStatuses.Complete
)。如果您已经有一个包含所需选项的表,那么您距离生成枚举仅有几个代码生成步骤。在 EntitySpaces 的友好人士提供的代码片段的帮助下,我能够将基于扩展属性的枚举生成添加到我的模板中。您所要做的就是在扩展属性中指定枚举名称以及用于数字和文本枚举值的列,然后以下模板代码将完成其余工作。请注意,EntityEnum
、EnumNameColumn
和 EnumValueColumn
方法的实现很简单,与上面的函数类似。
private System.Collections.Generic.List<string> EnumValues(ITable tbl)
{
if( tbl == null )
return null;
System.Collections.Generic.List<string> values =
new System.Collections.Generic.List<string>();
string nameColumn = EnumNameColumn(tbl);
string valueColumn = EnumValueColumn(tbl);
if( nameColumn == null || valueColumn == null )
return null;
string strSQL = string.Format(
"SELECT {0}, {1} FROM {2} ORDER BY {1}",
nameColumn, valueColumn, tbl.Name);
IDatabase database = MyMeta.Databases[databaseName];
ADODB.Recordset rs = database.ExecuteSql(strSQL);
if( rs != null && rs.RecordCount > 0 )
{
while( !rs.EOF )
{
values.Add(string.Format("{0} = {1}",
System.Text.RegularExpressions.Regex.Replace(
rs.Fields[0].Value.ToString().Trim(),
"[^A-Za-z0-9]", "_"),
rs.Fields[1].Value.ToString()));
rs.MoveNext();
}
rs.Close();
rs = null;
}
return values;
}
<%if (EntityEnum(table) != null) {%>
/// <summary>
/// Value enumeration for <%=table.Name%> table.
/// </summary>
public enum <%=EntityEnum(table)%>
{<%
System.Collections.Generic.List<string> enumValues = EnumValues(table);
for(int index = 0; index < enumValues.Count - 1; index++ ) {%>
<%=enumValues[index]%>,<%}%>
<%=enumValues[enumValues.Count - 1]%>
};
<%}%>
上面提供的代码模板片段为 Northwind Categories
表生成了以下枚举。
///
/// Value enumeration for Categories table.
///
public enum Categories
{
Beverages = 1,
Condiments = 2,
Confections = 3,
Dairy_Products = 4,
Grains_Cereals = 5,
Meat_Poultry = 6,
Produce = 7,
Seafood = 8
};
改进 NCover 覆盖率分析
与数据库驱动的代码模板无关,我想指出我修改后的代码模板中的另一个有用功能。我的团队有一个基于代码覆盖率的绩效指标,要求我们覆盖一定比例的自定义业务层代码。由于生成的代码太多,因此很难确定自定义代码覆盖率的实际数字,因为生成的代码也被测量了。因此,我向所有生成类的成员添加了 [GeneratedCode]
属性。您可以通过命令行参数 //ea 指示 NCover 忽略带有给定属性的成员,因此使用 //ea DAK.CustomESN.BusinessLayer.GeneratedCodeAttribute 来分析 CustomESNBusinessLayer 的覆盖率将仅为您提供自定义代码的覆盖率百分比。虽然这与当前主题无关,但我认为还是值得一提,因为它很有用,而且可能会让偶然发现此属性的不知情读者感到困惑。
有改进空间
如果我有时间,我会改进我的修改后的模板的一件事是为列添加扩展属性支持。我早就考虑过了,但从未在 EntitySpaces 模板生成的列名和类型上遇到问题,所以我没有更改任何内容。该框架旨在轻松获取列的扩展属性值。只需花些时间浏览所有模板文件,并将 esPlugin.PropertyName
的所有实例替换为我自己的 PropertyName
函数即可。另请注意,我的修改模板基于 2007 年 7 月发布的 EntitySpaces。如果您使用的是较新版本,则可能需要将我的修改模板与最新发布版本合并。
希望您喜欢这段关于使用扩展属性构建动态代码模板的简要介绍。如果您在通过 MyGeneration、EntitySpaces 或其他类似技术实现类似功能时遇到任何困难,我很乐意尽我所能提供帮助。
关于源文件
我没有在源代码中包含客户端,因为它并不能真正说明与本文相关的任何内容。您需要最新版本的 EntitySpaces 才能编译业务层,但编译不是必需的;代码主要是为了展示代码生成的结果。最有用的部分是 Templates 文件夹中修改过的 MyGeneration 模板,以及 NorthwindExtendedProperties.csv 中的 Northwind 扩展属性值。
修订历史
2008.01.03 - 初始版本
2008.01.11 - 从源 zip 文件中删除了 \bin 和 \obj 文件夹。由于生成的代码仅用于演示目的,并且在没有 EntitySpaces 的情况下无法构建,因此这些文件夹是不必要的。
许可说明
与 EntitySpaces 相关的代码可能包含下载文件本身的用法条款。本文中的所有其他代码均受以下许可的保护。