目录
2024/08/01
这是我第二次从头开始重写这个项目,这将是最后一次。我在2023年手头有一点空闲时间,所以我再次重新审视了这个项目。新版本将POCO Generator与其UI分离,提供了一个用于其他项目的类库,并增加了对MySQL的支持。您可以在 GitHub上找到POCO Generator v3。出于遗留目的,我将保留这篇文章。
如果您下载了源代码,但编译失败,并且收到有关ILMerge的错误,则需要取消阻止ILMerge。Windows对来自其他计算机的.exe文件采取安全措施并阻止它们,因此当您尝试编译代码时,Windows会阻止ILMerge运行,并且整个过程都失败了。转到packages\ILMerge.2.14.1208\tools,右键单击ILMerge.exe并打开属性窗口。点击取消屏蔽。编译。
介绍
有很多方法可以从数据库生成POCO类。困难的方法是手写它们。这可能适用于入门/一两个类方案,但不适用于生产环境。有代码生成工具,如 CodeSmith。该工具可检测对数据库的更改并生成相应的POCO类。Visual Studio支持 T4(文本模板转换工具包)等脚本工具。我选择创建的解决方案是一个以可视化为中心的独立应用程序,即POCO生成器,它遍历SQL Server,并从各种数据对象生成POCO。POCO生成器可以处理五种类型的数据库对象:
- 表
- 视图
- 存储过程
- 表值函数
- 用户定义表类型(TVP)
POCO生成器还可以检测表的主键、外键、唯一键和索引。
POCO生成器
SQL Server树列出了该实例上的所有数据库,每个数据库都列出了其数据对象——表、视图、过程、函数和TVP。树上的复选框用于选取要导出到文件的特定对象。窗口的右上角显示当前生成的POCO,具体取决于树中选择的内容。底部的面板允许您操作POCO的外观并处理将它们导出到文件的处理。当您更改这些选项时,POCO面板将刷新,您将立即看到POCO的外观。
POCO选项
POCO
POCO部分管理POCO的结构。
- 属性/数据成员——通常,POCO是使用属性构造的,但此选项提供了改用数据成员的选项。
- 虚拟属性——向属性添加虚拟修饰符
- 覆盖属性——向属性添加覆盖修饰符
- 部分类——向类添加部分修饰符
- 可为空的结构类型——所有struct类型都将变为可为null(int?,DateTime?),即使它们在数据库中不可为null。
- 注释&不带null ——针对每个属性的原始SQL Server类型以及它是否可为null的注释。没有null 将删除可为null的注释。
- Using——在POCO的开头添加using语句。
- 命名空间——使用指定的命名空间包装POCO。
- 继承——添加以逗号分隔的继承基类和接口列表。
- 列默认值——根据SQL Server中的列默认值添加属性初始化。无法正确处理的默认值将被注释。
- 成员之间的新行——如果POCO变大,尤其是使用EF批注时,此选项将在POCO属性之间添加行。
导航属性
导航属性通过解耦的POCO表示的外键连接它们。
- 导航属性——如有必要,添加导航属性和构造函数初始化。
- 虚拟——向导航属性添加虚拟修饰符。
- 覆盖——向导航属性添加替代修饰符。
- 显示多对多联接表——在多对多关系中,该join表默认处于隐藏状态。选中此选项后,将强制呈现该join表。
- 注释——原始SQL Server外键的注释。
- List,ICollection,IEnumerable——导航属性的类型(当它是POCO的集合时)。
类名
默认情况下,POCO类的名称是数据对象的名称,无论它是否是C#有效名称。“类名”部分操作该名称。
- 单数——将名称从复数更改为单数。仅适用于表格、视图和TVP。我试图在这里尽我所能,使用英语语法的单一规则,但显然它不是万无一失的。
- Include DB——添加数据库名称。
- 数据库分隔符——在数据库名称后添加指定的分隔符。
- 包括架构——添加架构名称。
- 忽略dbo架构——如果架构名称为“dbo”,则不添加架构名称。
- 架构分隔符 ——在架构名称后添加指定的分隔符。
- 单词分隔符——在类名中的单词之间添加指定的分隔符。单词被定义为下划线之间的文本或骆驼形的文本。
类名EmployeeDepartmentHistory中有3个单词,Employee、Department和History。类名Product_Category有2个单词,Product&Category。
- 驼峰式大小写,大写,小写——更改类名的大小写。
- 替换,使用,忽略大小写——对类名执行搜索和替换。
- 固定名称——忽略前面的所有选项,并将类的名称设置为指定的固定名称。
- 前缀和后缀——将前缀和后缀文本添加到类名中。
EF代码优先批注
“EF批注”部分将各种EF属性添加到POCO类及其属性中。仅适用于表。您可以在此MSDN页代码优先数据批注中阅读有关EF批注的详细信息。
- EF——添加EF主属性。
- Table属性。[Table("Production.Product")]
- 主键属性上的Key属性。[Key]
- Column复合主键属性的属性,其中Order值设置为复合主键中键的顺序。[Column(Order = 1)]
- string属性上的MaxLength属性。[MaxLength(50)]
- timestamp属性上的Timestamp属性。[Timestamp]
- DatabaseGenerated属性在Identity和Computed属性上。[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
- Column——添加具有Name和TypeName值的Column属性。[Column(Name = "ProductID", TypeName = "int")]
- Required——为不可为null的属性添加Required属性。 [Required]
- ErrorMessage是必需的——就像Required,但也添加了错误消息。 [Required(ErrorMessage = "Product ID is required")]
- ConcurrencyCheck——在Timestamp和RowVersion属性上添加ConcurrencyCheck属性。[ConcurrencyCheck]
- StringLength——在string属性上添加StringLength属性。此属性与数据库无关,这与MaxLength不同。它用作用户输入验证。[StringLength(50)]
- Display——添加Display属性。[Display(Name = "Product ID")]
- Description——将Description属性从SQL Server扩展属性表(MS_Description)添加到表和列中。[Description("Description from MS_Description")]
- ComplexType——根据SQL列名称中的第一个下划线,将属性分组到一个ComplexType中ComplexType_Column [ComplexType]
- Index(EF6)——为属性参与的每个索引添加Index属性。如果Index是唯一的或聚簇的,它还会设置Index相应的属性。EF不会生成聚集索引,但可以通过SSMS手动添加。适用于EF6及更高版本。[Index("IX_ProductName", IsUnique = true)]
- ForeignKey&InverseProperty——为导航属性添加ForeignKey属性。当两个POCO之间存在多个导航属性时添加InverseProperty属性。[ForeignKey("ProductID")] [InverseProperty("Product")]
导出到文件
将一个或多个POCO导出到一个或多个文件。
- 文件夹——指定要导出到的文件夹。
- 追加到文件——如果要将多个POCO导出到单个文件中,则很有用。如果未选中此选项,POCO生成器会将每个POCO导出到不同的文件。
- 导出按钮——如果SQL Server树上有选中的复选框,则POCO生成器将仅导出这些复选框。否则,它将导出当前选定的POCO。
其他按钮
- 复制——将当前选定的POCO复制到剪贴板。
- 命令行——当前选定选项和选中的数据库对象的命令行。
- 类型映射——SQL Server到.NET类型映射的弹出窗口。
命令行
命令行弹出窗口反映了当前选择的选项和在服务器树中选中的数据库对象。您也可以在编辑器中编辑命令行,但每次打开弹出窗口时都会重新生成命令行。帮助窗口描述命令行开关(短开关和长开关)和执行返回代码。您也可以将其保存到批处理文件中。
类型映射
您可以在此MSDN页 SQL Server数据类型映射中阅读有关类型映射的详细信息。
表和外键
右键单击一个表,上下文菜单将为您提供几个选项来检查通过外键连接到该表的任何其他表。
第一个选项(FK This -> To)检查表通过外键引用的所有表。外键在表中。第二个选项(FK From -> This)检查通过外键引用该表的所有表。外键位于其他表中。第三种选择是前两种选择的组合。递归选项执行前三个选项的作用,但将继续递归检查连接到连接到表的表的表,依此类推。
筛选结果
您可以通过右键单击组并从上下文菜单中选择“筛选”来筛选每个组(表、视图等)中的结果。在筛选器弹出窗口中,选择要包含或排除的名称和架构。
具有许多结果集的存储过程
无法确定存储过程是否返回多个结果集。在检索存储过程的架构的过程中,仅返回第一个结果集。在第一个结果集之后,无法访问任何结果集的架构。
“解决方案”比其他任何事情都更像是一种黑客。在存储过程中,标记第一个select查询并更改存储过程。然后,转到UI并右键单击存储过程。单击上下文菜单中的刷新。出现新的POCO后,复制或导出它以供进一步使用。继续此过程,直到最后一个结果集。完成后,撤消备注并还原存储过程。
模式
检索SQL Server数据对象架构的过程主要通过DbConnection类中的GetSchema方法完成。SqlConnection继承自的DbConnection类有几种GetSchema方法,它们完全按照其名称所暗示的那样。它们从指定的数据源返回架构信息。你可以传递给GetSchema方法,你正在寻找的对象类型和限制列表通常用于筛选数据库名称,模式名称和对象名称。可以在这些MSDN页上找到对象类型和限制的完整列表:架构集合和架构限制。
表格和视图
表和视图的架构类型均为“Tables”。对于表,将string “BASE TABLE”放在最后一个限制上,这是表类型限制。对于视图,请在表类型限制上加上string “VIEW”。
表:
using (SqlConnection connection = new SqlConnection(connectionString))
{
connection.Open();
DataTable allTables = connection.GetSchema("Tables",
new string[] { database_name, null, null, "BASE TABLE" });
DataTable specificTable = connection.GetSchema("Tables",
new string[] { database_name, schema_name, table_name, "BASE TABLE" });
}
和视图:
using (SqlConnection connection = new SqlConnection(connectionString))
{
connection.Open();
DataTable allViews = connection.GetSchema("Tables",
new string[] { database_name, null, null, "VIEW" });
DataTable specificView = connection.GetSchema("Tables",
new string[] { database_name, schema_name, view_name, "VIEW" });
}
用户定义表类型(TVP)
TVP架构无法通过GetSchema方法检索,或者至少无法可靠检索。获取TVP架构需要在SQL Server端进行一些查询。第一个查询获取数据库上的所有TVP。
select
tvp_schema = ss.name,
tvp_name = stt.name,
stt.type_table_object_id
from sys.table_types stt
inner join sys.schemas ss on stt.schema_id = ss.schema_id
对于每个TVP,我们都会得到它的列列表。@tvp_id参数是上一个查询中的type_table_object_id列。
select
sc.*,
data_type = st.name
from sys.columns sc
inner join sys.types st on sc.system_type_id = st.system_type_id and sc.user_type_id = st.user_type_id
where sc.object_id = @tvp_id
存储过程和表值函数
存储过程和函数的架构类型均为“Procedures”。对于存储过程,将string “PROCEDURE”放在最后一个限制上,这是例程类型限制。对于函数,将string “FUNCTION”放在例程类型限制上。
存储过程:
using (SqlConnection connection = new SqlConnection(connectionString))
{
connection.Open();
DataTable allProcedures = connection.GetSchema("Procedures",
new string[] { database_name, null, null, "PROCEDURE" });
DataTable specificProcedure = connection.GetSchema("Procedures",
new string[] { database_name, schema_name, procedure_name, "PROCEDURE" });
}
和函数:
using (SqlConnection connection = new SqlConnection(connectionString))
{
connection.Open();
DataTable allFunctions = connection.GetSchema("Procedures",
new string[] { database_name, null, null, "FUNCTION" });
DataTable specificFunction = connection.GetSchema("Procedures",
new string[] { database_name, schema_name, function_name, "FUNCTION" });
}
对于每个例程,我们需要获取其参数。架构类型为“ProcedureParameters”。
using (SqlConnection connection = new SqlConnection(connectionString))
{
connection.Open();
DataTable routineParameters = connection.GetSchema("ProcedureParameters",
new string[] { database_name, routine_schema, routine_name, null });
}
此时,我们可以过滤掉任何不是Table-valued函数的东西,这意味着我们需要删除标量函数。标量函数有一个返回参数,它是函数的结果,这就是我们找到它们的方式。
一旦我们有了例程参数,我们将为每个参数构建一个空SqlParameter参数。空SqlParameter是一个将DBNull.Value设置为其值的参数。对于TVP参数,我们将构建一个SqlDbType.Structured类型的参数,并将一个空的DataTable作为其值。
这是一个关于如何构建SqlParameter的非常简短的代码片段。
SqlParameter sqlParameter = new SqlParameter();
// name
sqlParameter.ParameterName = parameter_name;
// empty value
sqlParameter.Value = DBNull.Value;
// type
switch (data_type)
{
case "bigint": sqlParameter.SqlDbType = SqlDbType.BigInt; break;
case "binary": sqlParameter.SqlDbType = SqlDbType.VarBinary; break;
....
case "varchar": sqlParameter.SqlDbType = SqlDbType.VarChar; break;
case "xml": sqlParameter.SqlDbType = SqlDbType.Xml; break;
}
// size for string type
// character_maximum_length comes from the parameter schem
if (data_type == "binary" || data_type == "char" ||
data_type == "nchar" || data_type == "nvarchar" ||
data_type == "varbinary" || data_type == "varchar")
{
if (character_maximum_length == -1 || character_maximum_length > 0)
sqlParameter.Size = character_maximum_length;
}
// direction
if (parameter_mode == "IN")
sqlParameter.Direction = ParameterDirection.Input;
else if (parameter_mode == "INOUT")
sqlParameter.Direction = ParameterDirection.InputOutput;
else if (parameter_mode == "OUT")
sqlParameter.Direction = ParameterDirection.Output;
现在,我们已准备好获取例程的列。当涉及到例程时,我们将使用SqlDataReader.GetSchemaTable()方法获取带有CommandBehavior.SchemaOnly标志的例程模式。
对于存储过程,我们可以使用CommandType.StoredProcedure。
using (SqlConnection connection = new SqlConnection(connectionString))
{
using (SqlCommand command = new SqlCommand())
{
command.Connection = connection;
command.CommandText = string.Format("[{0}].[{1}]", routine_schema, routine_name);
command.CommandType = CommandType.StoredProcedure;
// for each routine parameter, build it and add it to command.Parameters
using (SqlDataReader reader = command.ExecuteReader(CommandBehavior.SchemaOnly))
{
DataTable schemaTable = reader.GetSchemaTable();
}
}
}
对于Table-valued函数,我们需要构造一个从函数中选择所有列的查询。
using (SqlConnection connection = new SqlConnection(connectionString))
{
using (SqlCommand command = new SqlCommand())
{
command.Connection = connection;
command.CommandType = CommandType.Text;
command.CommandText = string.Format("select * from [{0}].[{1}](", routine_schema, routine_name);
// for each routine parameter, build it and add it
// to command.Parameters and add its name to command.CommandText
command.CommandText += ")";
using (SqlDataReader reader = command.ExecuteReader(CommandBehavior.SchemaOnly))
{
DataTable schemaTable = reader.GetSchemaTable();
}
}
}
主键和外键
我使用 SQL 查询来检索有关主键和外键的信息。该GetSchema方法不够详细。
下面给出的是主键的查询。
select
Name = kc.name,
Schema_Name = ss.name,
Table_Name = object_name(kc.parent_object_id),
Ordinal = ic.key_ordinal,
Column_Name = c.name,
Is_Descending = ic.is_descending_key,
Is_Identity = c.is_identity,
Is_Computed = c.is_computed
from sys.key_constraints kc
inner join sys.index_columns ic
on kc.parent_object_id = ic.object_id and kc.unique_index_id = ic.index_id and kc.type = 'PK'
inner join sys.columns c on ic.object_id = c.object_id and ic.column_id = c.column_id
inner join sys.schemas ss on kc.schema_id = ss.schema_id
order by Schema_Name, Table_Name, Ordinal
下面给出的是外键查询。
select
Name = f.name,
Foreign_Schema = ssf.name,
Foreign_Table = object_name(f.parent_object_id),
Foreign_Column = col_name(fc.parent_object_id, fc.parent_column_id),
Primary_Schema = ssp.name,
Primary_Table = object_name (f.referenced_object_id),
Primary_Column = col_name(fc.referenced_object_id, fc.referenced_column_id),
Ordinal = fc.constraint_column_id
from sys.foreign_keys f
inner join sys.foreign_key_columns fc on f.object_id = fc.constraint_object_id
inner join sys.schemas ssf on f.schema_id = ssf.schema_id
inner join sys.tables st on f.referenced_object_id = st.object_id
inner join sys.schemas ssp on st.schema_id = ssp.schema_id
order by Foreign_Schema, Foreign_Table, Ordinal
导航属性
导航属性定义POCO之间的关系,是数据库表之间外键的反映。有3种类型的关系:一对多、一对一、多对多。
我将SQL文件Keys.sql附加到源代码作为解决方案项。当在任何数据库中执行时,它将输出数据库的主键、唯一键和外键。对于外键,它还将输出它们的关系类型和其他一些有用的属性。
一对多关系
没有任何特殊约束的单个外键是两个表之间一对多关系的数据库实现。
在此示例中,外键为从Product.ProductModelID到ProductModel.ProductModelID。ProductModelID是ProductModel表的主键。此外键定义Product和ProductModel之间的一对多关系。Product POCO具一个指向ProductModel的单一导航属性,而ProductModel POCO具有Product的集合导航属性。
public class Product
{
public int ProductID { get; set; } // primary key
public int? ProductModelID { get; set; } // foreign key
public virtual ProductModel ProductModel { get; set; }
}
public class ProductModel
{
public ProductModel()
{
this.Products = new HashSet<Product>();
}
public int ProductModelID { get; set; } // primary key
public virtual ICollection<Product> Products { get; set; }
}
一对一关系
一对一关系的数据库实现是指一个表的主键也是另一个表的主键的外键。POCO生成器无法识别一对一关系的唯一键/唯一索引数据库实现。一对一关系的SQL Server实现在技术上是1对0或1。
在此示例中,外键为从Employee.BusinessEntityID到Person.BusinessEntityID。Person.BusinessEntityID是Person的主键,Employee.BusinessEntityID既是Employee的主键,又是Person的主键的外键。
public class Employee
{
public int BusinessEntityID { get; set; } // primary key, foreign key
public virtual Person Person { get; set; }
}
public class Person
{
public int BusinessEntityID { get; set; } // primary key
public virtual Employee Employee { get; set; }
}
多对多关系
多对多关系是指两个或多个实体对关系中的所有其他实体具有多个引用。多对多关系的数据库实现是一个join表,或者直观地说是一个“中间”的表,它是参与该关系的所有表的所有主键的构造。join表中的每个主键也是另一个相应表中相应主键的外键。多对多关系中的表不直接相互引用,而是遍历join表,因此表“在中间”。
如果join表的列数多于其他主键的外键数(例如,创建时间列),则POCO生成器会将此关系视为join表与关系中其他每个表之间的一对多关系。当选中“显示多对多连接表”选项时,这也将生效。
在此示例中,一个产品可以位于多个仓库中,每个仓库存储许多不同的产品。join表为WarehouseProducts。WarehouseProducts的所有列都是主键,每列都是产品主键或仓库主键的外键。
public class Product
{
public Product()
{
this.Warehouses = new HashSet<Warehouse>();
}
public int ProductID { get; set; } // primary key
public virtual ICollection<Warehouse> Warehouses { get; set; }
}
// this poco is not rendered. only for illustration
public class WarehouseProducts
{
public int ProductID { get; set; } // primary key, foreign key
public int WarehouseID { get; set; } // primary key, foreign key
}
public class Warehouse
{
public Warehouse()
{
this.Products = new HashSet<Product>();
}
public int WarehouseID { get; set; } // primary key
public virtual ICollection<Product> Products { get; set; }
}