目录
VisualBasicSourceCodeGenerator
深入了解DatabaseContext、DatabaseFactory、Database类
消息
2021-12-31我将所有源代码更新为.NET6。
.NET6上的DbSharp支持DateOnly、TimeOnly到Date、Time类型的数据库。
DbSharp删除了对Microsoft.Data.SqlTypes的支持,因为它们在.NET6上不受支持
您可以下载DbSharpApplicationXXXXXXXX.zip。
https://github.com/higty/higlabo/tree/master/Net6/Tools/DbSharp/Compiled
请下载最新版本的.zip文件。这些天我继续修复一些错误。
概括
本文通过创建DAL生成器(不是ORM ...)来轻松调用存储过程,向您介绍OOP、TestFirst、SRP。我将向您展示实现。作为起点,请知道我使用Visual Studio 2022和SQL Server。
DbSharp是一个DAL生成器。它生成一个StoredProcedure客户端类,使您能够轻松调用存储过程。DbSharp还创建了使您能够进行CRUD操作的Table和Record类。
我们向GitHub和Nuget发布了.NET6版本。
您可以使用除DbSharp之外的其他ORM库(NHibernate、EntityFramework、DataObject.net),但如果您喜欢DbSharp,则可以免费使用。
如何使用DbSharp?
您可以 从以下链接下载.NET6上的 DbSharpApplication:
示例代码:
以下是使用DbSharp的列表:
- 设置示例数据库
- 启动DbSharpApplication.exe
- 选择DatabaseType(SqlServer, MySql)并输入连接字符串
- 导入StoredProcedure, UserDefinedType, Table。菜单栏->编辑->导入XXX
- 生成C#代码。菜单栏->编辑->生成C#代码
- 通过Visual Studio创建类库项目,添加生成的文件并编译成DLL
- 在您的应用程序中使用DLL
在示例项目(DbSharpSample.sln)中,您可以看到解决方案文件包括:
- HigLaboSample.Data.MySql.CSharp(带有生成的文件的ClassLibrary)
- HigLaboSample.Data.SqlServer.CSharp(带有生成的文件的ClassLibrary)
- HigLaboSampleApp.MultiDatabase(一些Console应用程序......)
- HigLaboSampleApp.MySql(用于MySql)
- HigLaboSampleApp.SqlServer(用于SqlServer)
这些项目是通过以下步骤创建的:
首先,我通过Management Studio创建了一个DbSharpSample数据库,并对这个数据库执行脚本DbSharp\HigLaboSample.Data.SqlServer.CSharp\DbSharp_SqlServer.sql。
然后启动DbSharpApplication.exe并选择目标数据库到SqlServer。
选择菜单栏->编辑->管理连接。
添加数据库的连接字符串。
选择菜单栏->编辑->读取数据库方案
按连接按钮,确保选择所有对象,然后按执行按钮。
Table, StoredProcedure,UserDefinedTableType被导入。您可以通过单击Table选项卡看到导入的Table。
您可以通过单击StoredProcedure选项卡看到导入的StoredProcedure。
您可以通过单击UserDefinedTableType选项卡看到导入的UserDefinedTableType。
导入表时,每1个Table生成5个StoredProcedure用于CRUD操作。
- SelectAll
- SelectByPrimaryKey
- Insert
- Update
- Delete
您可以通过单StoredProcedure击选项卡来查看这些StoredProcedure。
您可以管理Enum类型到列。设置EnumName=MyEnum为AllDataTypeTable的EnumColumn, NotNullEnumColumn。
您可以确认EnumName是否设置为自动生成StroedProcedure。
您可以看Usp_SelectMultiTable返回多个ResultSets到向客户端。
您可以将这些类名称从ResultSetX更改为您自己的名称。
通过MenuBar -> File -> Save将这些模式保存为文件。您可以通过MenuBar -> File -> Open File加载文件。
现在,您可以通过MenuBar -> Edit -> Generate C# Code从这些模式生成C#源代码。
- 设置输出C#源代码文件的路径
- 生成文件的RootNamespace
- DatabaseKey
我稍后会解释DatabaseKey。按下执行按钮,在输出目录路径中生成C#文件。
创建新的类库项目,并添加这些生成的文件:
像这样定义MyEnum:
public enum MyEnum
{
Default,
Value1,
Value2,
}
并添加MyEnum到类库:
添加对HigLabo.Core, HigLabo.Data,HigLabo.DbSharp的引用。如果使用SqlGeometry, SqlGeography, HierarchyId,则必须添加Microsoft.SqlServer.Types。
并编译它。创建HigLaboSample.Data.SqlServer.CSharp.dll。
创建名为HigLaboSampleApp.SqlServer的新控制台应用程序,添加引用HigLabo.Core, HigLabo.Data, HigLabo.DbSharp, Microsoft.SqlServer.Types(可选...), HigLaboSample.Data.SqlServer.CSharp。
现在你可以像这样执行:
var ss = Environment.GetCommandLineArgs();
String connectionString = ss[1];
DatabaseFactory.Current.SetCreateDatabaseMethod
("DbSharpSample", () => new SqlServerDatabase(connectionString));
AllDataTypeTable t = new AllDataTypeTable();
var r = new AllDataTypeTable.Record();
r.PrimaryKeyColumn = 11;
r.IntColumn = 2;
//Set properties...
var x1 = t.Insert(r);
您可以通过DbSharpSample.sln阅读后面的部分以了解DbSharp。
测试优先开发
本节通过为生成的C#代码创建测试用例来解释“测试优先”开发。本节的目标是生成一个示例类,如下所示:
public class Person
{
private Int32 _Age = 0;
public String Name { get; set; }
public Int32 Age
{
get { return _Age; }
}
public List<Person> Children { get; private set; }
public Person(String name)
{
this.Children = new List<Person>();
this.Name = name;
}
public void ShowName()
{
Console.WriteLine(this.Name);
}
public void AddAge(Int32 value)
{
_Age += value;
}
}
我像下面的元素一样划分这个类,并为每个元素创建生成器:
- TypeName
- Field
- AccessModifier
- MethodAccessModifier
- FieldModifier
- ConstructorModifier
- Constructor
- MethodModifier
- MethodParameter
- Method
- PropertyBody
- Property
- ClassModifier
- Class
- InterfaceProperty
- InterfaceMethod
- Interface
- Namespace
- SourceCode
TypeName涵盖诸如Int32,Person,List<String>,Dictionary<String, List<Person>>之类的Type名称。起初,我创建了一个关于TypeName类的测试用例(参见HigLabo.CodeGenerator.Version0项目)。
[TestClass]
public class TypeNameTest
{
[TestMethod]
public void TypeNameWithoutGenericTypes()
{
var tp = new TypeName("Int32");
Assert.AreEqual("Int32", tp.Write());
}
[TestMethod]
public void TypeNameWithGenericTypes()
{
var tp = new TypeName("Func");
tp.GenericTypes.Add(new TypeName("String"));
tp.GenericTypes.Add(new TypeName("Int32"));
Assert.AreEqual("Func<String, Int32>", tp.Write());
}
[TestMethod]
public void TypeNameWithNestedGenericTypes()
{
var tp = new TypeName("Func");
tp.GenericTypes.Add(new TypeName("String"));
var tp1 = new TypeName("Action");
tp1.GenericTypes.Add(new TypeName("String"));
tp1.GenericTypes.Add(new TypeName("Int32"));
tp.GenericTypes.Add(tp1);
Assert.AreEqual("Func<String, Action<String, Int32>>", tp.Write());
}
}
我创建了TypeName类来管理类型信息并生成源代码以通过测试用例。
public class TypeName
{
public String Name { get; set; }
public List<TypeName> GenericTypes { get; private set; }
public TypeName(String name)
{
this.Name = name;
this.GenericTypes = new List<TypeName>();
}
public String Write()
{
StringBuilder sb = new StringBuilder();
sb.Append(this.Name);
if (this.GenericTypes.Count > 0)
{
sb.Append("<");
for (int i = 0; i < this.GenericTypes.Count; i++)
{
var tp = this.GenericTypes[i];
sb.Append(tp.Write());
if (i < this.GenericTypes.Count - 1)
{
sb.Append(", ");
}
}
sb.Append(">");
}
return sb.ToString();
}
}
并运行测试。您可以确认所有测试均已通过。我创建了另一个元素,您可以看到如何实现和测试它。
单一职责原则
本节解释单一职责原则,以实现针对多语言支持的高效工作。我也计划实现一个VB版本。但是我在上一节的类设计中发现了一个设计问题。要添加一个VB版本,我将更改TypeName类,如下所示:
public class TypeName
{
public String Name { get; set; }
public List<TypeName> GenericTypes { get; private set; }
public TypeName(String name)
{
this.Name = name;
this.GenericTypes = new List<TypeName>();
}
public String WriteCSharp()
{
//..method body
}
public String WriteVB()
{
//..method body
}
}
如果您计划添加Java、F#或其他语言,请将WriteXXX方法添加到TypeName类中。这会导致一些问题。C#程序员和VB程序员必须针对同一个文件工作。如果每个程序员都针对不同的文件工作以避免版本控制问题,可能会更好。想象一下,一个程序员在美国工作,另一个在日本工作,并且由于某种原因(不同的公司、恶劣的环境等),他们没有通过同一个TFS共享源代码。所以,我重新设计了TypeName类以仅管理信息数据和SourceCodeGenerator类只处理生成源代码文本和格式设置。如果不分,TypeName类会有一个关于格式设置的属性(例如,插入换行符自动属性getter)。
重新设计后,两个类将负责单一功能。我为它创建了一个测试用例(也许为C#和VB划分一个测试用例类会更好,但我没有时间去做)。
[TestClass]
public class TypeNameTest
{
[TestMethod]
public void TypeNameWithoutGenericTypes()
{
var tp = new TypeName("Int32");
{
var g = new CSharpSourceCodeGenerator();
Assert.AreEqual("Int32", g.Write(tp));
}
{
var g = new VisualBasicSourceCodeGenerator();
Assert.AreEqual("Int32", g.Write(tp));
}
}
[TestMethod]
public void TypeNameWithGenericTypes()
{
var tp = new TypeName("Func");
tp.GenericTypes.Add(new TypeName("String"));
tp.GenericTypes.Add(new TypeName("Int32"));
{
var g = new CSharpSourceCodeGenerator();
Assert.AreEqual("Func<String, Int32>", g.Write(tp));
}
{
var g = new VisualBasicSourceCodeGenerator();
Assert.AreEqual("Func(Of String, Int32)", g.Write(tp));
}
}
[TestMethod]
public void TypeNameWithNestedGenericTypes()
{
var tp = new TypeName("Func");
tp.GenericTypes.Add(new TypeName("String"));
var tp1 = new TypeName("Action");
tp1.GenericTypes.Add(new TypeName("String"));
tp1.GenericTypes.Add(new TypeName("Int32"));
tp.GenericTypes.Add(tp1);
{
var g = new CSharpSourceCodeGenerator();
Assert.AreEqual("Func<String, Action<String, Int32>>", g.Write(tp));
}
{
var g = new VisualBasicSourceCodeGenerator();
Assert.AreEqual("Func(Of String, Action(Of String, Int32))", g.Write(tp));
}
}
}
所以,我创建了一个CSharpSourceCodeGenerator类来解决上述问题(参见HigLabo.CodeGenerator.Version1项目)。
public class CSharpSourceCodeGenerator
{
public CSharpSourceCodeGenerator()
{
}
public String Write(TypeName typeName)
{
StringBuilder sb = new StringBuilder();
sb.Append(typeName.Name);
if (typeName.GenericTypes.Count > 0)
{
sb.Append("<");
for (int i = 0; i < typeName.GenericTypes.Count; i++)
{
var tp = typeName.GenericTypes[i];
sb.Append(this.Write(tp));
if (i < typeName.GenericTypes.Count - 1)
{
sb.Append(", ");
}
}
sb.Append(">");
}
return sb.ToString();
}
}
而VisualBasicSourceCodeGenerator如下所示:
public class VisualBasicSourceCodeGenerator
{
public String Write(TypeName typeName)
{
StringBuilder sb = new StringBuilder();
sb.Append(typeName.Name);
if (typeName.GenericTypes.Count > 0)
{
sb.Append("(Of ");
for (int i = 0; i < typeName.GenericTypes.Count; i++)
{
var tp = typeName.GenericTypes[i];
sb.Append(this.Write(tp));
if (i < typeName.GenericTypes.Count - 1)
{
sb.Append(", ");
}
}
sb.Append(")");
}
return sb.ToString();
}
}
现在TypeName类如下。之前有点简单。
public class TypeName
{
public String Name { get; set; }
public List<TypeName> GenericTypes { get; private set; }
public TypeName(String name)
{
this.Name = name;
this.GenericTypes = new List<TypeName>();
}
}
现在文件被分离了,两个程序员都可以处理每个文件而没有版本控制的痛苦(或者至少比以前少痛苦)。并且您可以在不更改元数据类(TypeName、Field等)的情况下扩展CodeGenerator类(例如格式设置)。如果需要,您可以从元数据类扩展HTML生成器。或者您可以轻松地划分像HigLabo.ClassSchema.dll、HigLabo.CSharpCodeGenerator.dll、HigLabo.HtmlGenerator.dll这样的DLL 。
StringBuilder与TextWriter
本节说明这TextWriter比StringBuilder性能改进要好。在您开始创建其他元素(Field、AccessModifier等)之前,我会更改CSharpSourceCodeGenerator设计。将来,我将从StoredProcedure模式中生成许多文件。
我还必须考虑性能、内存使用和文件I/O。StringBuilder优于String,但是一旦调用ToString方法,StringBulider就会将数据分配到堆内存中。如果使用TextWriter,则可以像这样将文本数据目录输出到文件系统。
using (TextWriter writer = File.CreateText("MyFile.txt"))
{
writer.WriteLine("my data");
}
由于上述原因,我像这样更改测试用例(StringWriter类继承TextWriter类)。
[TestClass]
public class TypeNameTest
{
[TestMethod]
public void TypeNameWithoutGenericTypes()
{
var tp = new TypeName("Int32");
Assert.AreEqual("Int32", SourceCodeGenerator.Write(SourceCodeLanguage.CSharp, tp));
Assert.AreEqual("Int32", SourceCodeGenerator.Write(SourceCodeLanguage.VB, tp));
}
[TestMethod]
public void TypeNameWithGenericTypes()
{
var tp = new TypeName("Func");
tp.GenericTypes.Add(new TypeName("String"));
tp.GenericTypes.Add(new TypeName("Int32"));
Assert.AreEqual("Func<String, Int32>",
SourceCodeGenerator.Write(SourceCodeLanguage.CSharp, tp));
Assert.AreEqual("Func(Of String, Int32)",
SourceCodeGenerator.Write(SourceCodeLanguage.VB, tp));
}
[TestMethod]
public void TypeNameWithNestedGenericTypes()
{
var tp = new TypeName("Func");
tp.GenericTypes.Add(new TypeName("String"));
var tp1 = new TypeName("Action");
tp1.GenericTypes.Add(new TypeName("String"));
tp1.GenericTypes.Add(new TypeName("Int32"));
tp.GenericTypes.Add(tp1);
Assert.AreEqual("Func<String, Action<String, Int32>>"
, SourceCodeGenerator.Write(SourceCodeLanguage.CSharp, tp));
Assert.AreEqual("Func(Of String, Action(Of String, Int32))"
, SourceCodeGenerator.Write(SourceCodeLanguage.VB, tp));
}
}
并像这样更改CodeGenerator类。
CSharpSourceCodeGenerator
public class CSharpSourceCodeGenerator : SourceCodeGenerator
{
public override SourceCodeLanguage Language
{
get { return SourceCodeLanguage.CSharp; }
}
public CSharpSourceCodeGenerator(TextWriter textWriter)
: base(textWriter)
{
}
public override void Write(TypeName typeName)
{
var writer = this.TextWriter;
writer.Write(typeName.Name);
if (typeName.GenericTypes.Count > 0)
{
writer.Write("<");
for (int i = 0; i < typeName.GenericTypes.Count; i++)
{
var tp = typeName.GenericTypes[i];
this.Write(tp);
if (i < typeName.GenericTypes.Count - 1)
{
writer.Write(", ");
}
}
writer.Write(">");
}
}
//Other method...
}
VisualBasicSourceCodeGenerator
public class VisualBasicSourceCodeGenerator: SourceCodeGenerator
{
public override SourceCodeLanguage Language
{
get { return SourceCodeLanguage.VB; }
}
public VisualBasicSourceCodeGenerator(TextWriter textWriter)
: base(textWriter)
{
}
public override void Write(TypeName typeName)
{
var writer = this.TextWriter;
writer.Write(typeName.Name);
if (typeName.GenericTypes.Count > 0)
{
writer.Write("(Of ");
for (int i = 0; i < typeName.GenericTypes.Count; i++)
{
var tp = typeName.GenericTypes[i];
this.Write(tp);
if (i < typeName.GenericTypes.Count - 1)
{
writer.Write(", ");
}
}
writer.Write(")");
}
}
//Other method...
}
SourceCodeGenerator abstract基类具有XXXSourceCodeGenerator类的共同属性和方法。
public abstract class SourceCodeGenerator
{
public String Indent { get; set; }
public Int32 CurrentIndentLevel { get; set; }
public TextWriter TextWriter { get; private set; }
public abstract SourceCodeLanguage Language { get; }
protected SourceCodeGenerator(TextWriter textWriter)
{
this.Indent = " ";
this.CurrentIndentLevel = 0;
this.TextWriter = textWriter;
}
public abstract void Write(TypeName typeName);
public abstract void Write(CodeBlock codeBlock);
public abstract void Write(AccessModifier modifier);
public abstract void Write(MethodAccessModifier modifier);
public abstract void Write(FieldModifier modifier);
public abstract void Write(Field field);
public abstract void Write(ConstructorModifier modifier);
public abstract void Write(Constructor constructor);
public abstract void Write(MethodModifier modifier);
public abstract void Write(MethodParameter parameter);
public abstract void Write(Method method);
public abstract void Write(PropertyBody propertyBody);
public abstract void Write(Property property);
public abstract void Write(ClassModifier modifier);
public abstract void Write(Class @class);
public abstract void Write(InterfaceProperty property);
public abstract void Write(InterfaceMethod method);
public abstract void Write(Interface @interface);
public abstract void Write(Namespace @namespace);
public abstract void Write(SourceCode sourceCode);
protected void WriteIndent()
{
for (int i = 0; i < this.CurrentIndentLevel; i++)
{
this.TextWriter.Write(this.Indent);
}
}
protected void WriteLineAndIndent()
{
this.WriteLineAndIndent("");
}
protected void WriteLineAndIndent(String text)
{
this.TextWriter.WriteLine(text);
for (int i = 0; i < this.CurrentIndentLevel; i++)
{
this.TextWriter.Write(this.Indent);
}
}
public void Flush()
{
this.TextWriter.Flush();
}
}
您可以看到这些类HigLabo.CodeGenerator项目的完整C#实现。您还可以确认HigLabo.CodeGenerator.Sample项目生成的C#文本。VB版未完成。如果您是VB程序员,如果有人创建了VB版本的测试用例和代码生成器,我将不胜感激。
设计存储过程类
在本章中,我将向您展示如何使用C#和OOP设计存储过程客户端类。有两种类型的存储过程。一个只对数据库执行命令,另一个从数据库中获取数据。我通过这个脚本创建了一个示例表和存储过程。
Create Table MyTaskTable
(TaskId UniqueIdentifier not null
,Title Nvarchar(100) Not Null
,[Priority] Int Not Null
,[State] Nvarchar(10) Not Null
,CreateTime Datetime Not Null
,ScheduleDate Date
,Detail Nvarchar(max) Not Null
,TimestampColumn Timestamp
)
Go
Alter Table [dbo].[MyTaskTable] Add Constraint [PK_MyTaskTable]
Primary Key Clustered (TaskId)
Go
Create Procedure MyTaskTableInsert
(@TaskId UniqueIdentifier
,@Title Nvarchar(100)
,@Priority Int
,@State Nvarchar(10)
,@CreateTime Datetime
,@ScheduleDate Date
,@Detail Nvarchar(max)
) As
Insert Into MyTaskTable
(TaskId, Title, [Priority], [State], CreateTime, ScheduleDate, Detail)
Values (@TaskId, @Title, @Priority, @State, @CreateTime, @ScheduleDate, @Detail)
Go
Create Procedure MyTaskTableSelectBy_TaskId
(@TaskId UniqueIdentifier
) As
select * from MyTaskTable with(nolock)
where TaskId = @TaskId
Go
您必须创建一些代码来调用存储过程。我想生成一个调用存储过程的代码。我从调用者位置设计这些类,如下所示:
var db = new HigLabo.Data.SqlServerDatabase("connection string");
//Execute stored procedure
var sp = new MyTaskTableInsert();//Same name to stored procedure on database
sp.TaskId = Guid.NewGuid(); //Strongly typed property corresponding to
//stored procedure's parameter
sp.Title = "Post article to CodeProject";
sp.Priority = 2;
sp.State = "Executing";
sp.CreateTime = DateTime.Now;
sp.ScheduleDate = new DateTime(2014, 3, 25);
sp.Detail = "...Draft...";
//Execute MyTaskTableInsert stored procedure on database and get affected record count.
var result = sp.ExecuteNonQuery(db);
//or call like this
//var result1 = db.Execute(sp);
var sp1 = new MyTaskTableSelectBy_TaskId();
sp1.TaskId = sp.TaskId;
var recordList = sp.GetResultSets(); //Get list of POCO objects that represent
//a record of table on database.
foreach(var record in recordList)
{
//Do something...
}
使用起来看起来很直观。与DataTable或DataReader相比,它是强类型的。由于强类型属性,可以通过智能感知获得很大帮助。
您可以像这样对多数据库(具有相同架构)执行存储过程:
var db1 = new HigLabo.Data.SqlServerDatabase("connection string to DB1");
var db2 = new HigLabo.Data.SqlServerDatabase("connection string to DB2");
//Execute stored procedure
var sp = new MyTaskTableInsert();
sp.TaskId = Guid.NewGuid();
sp.Title = "Post article to CodeProject";
sp.Priority = 2;
sp.State = "Executing";
sp.CreateTime = DateTime.Now;
sp.ScheduleDate = new DateTime(2014, 3, 25);
sp.Detail = "...Draft...";
var db1Result1 = sp.ExecuteNonQuery(db1);
var db2Result1 = sp.ExecuteNonQuery(db2);
//Or call like this
var db1Result2 = db1.Execute(sp);
var db2Result2 = db2.Execute(sp);
接下来,我考虑transaction。我设计的Transaction功能是这样的:
var db = new HigLabo.Data.SqlServerDatabase("connection string");
using (TransactionContext tx = new TransactionContext(db))
{
tx.BeginTransaction(IsolationLevel.ReadCommitted);
for (int i = 0; i < 3; i++)
{
var sp = new MyTaskTableInsert();
//...Set property of MyTaskTableInsert object
var result = sp.ExecuteNonQuery(tx);
}
tx.CommitTransaction();
}
有时,您可能会对数据库执行包含多个事务的命令。我将设计多事务功能,如下所示:
var db1 = new HigLabo.Data.SqlServerDatabase("connection string to DB1");
var db2 = new HigLabo.Data.SqlServerDatabase("connection string to DB2");
using (TransactionContext tx1 = new TransactionContext(db1)))
{
using (TransactionContext tx2 = new TransactionContext(db2)))
{
tx1.BeginTransaction(IsolationLevel.ReadCommitted);
tx2.BeginTransaction(IsolationLevel.ReadCommitted);
for (int i = 0; i < 3; i++)
{
var sp = new MyTaskTable1Insert();
//...Set property of MyTaskTableInsert object
var result = sp.ExecuteNonQuery(tx1);
}
for (int i = 0; i < 3; i++)
{
var sp = new MyTaskTable2Insert();
//...Set property of MyTaskTableInsert object
var result = sp.ExecuteNonQuery(tx2);
}
tx1.CommitTransaction();
tx2.CommitTransaction();
}
}
多数据库,Transaction受上述规范支持。这些是数据库访问库的非常基本的功能。
接下来,我考虑使用一个数据库的情况。我经常遇到这种情况,尤其是通过创建小型Web应用程序。然后,我觉得为每个StoredProcedure.ExecuteNonQuery,GetResultSet方法分配Database对象是多余的。所以,我添加了默认数据库功能。您可以通过调用DatabaseFactory对象的SetCreateDatabaseMethod方法来设置默认数据库。ExecuteNonQuery, GetResultSets方法将执行到您指定的SetCreateDatabaseMethod方法的Database。如果您使用一个数据库,这种设计会使事情变得简单。
//Call once on application start what database you use...
DatabaseFactory.Current.SetCreateDatabaseMethod
("DbSharpSample", () => new HigLabo.Data.SqlServerDatabase("connection string to db"));
var sp = new MyTaskTableInsert();
//sp.GetDatabaseKey() returns "DbSharpSample".
//You can specify DatabaseKey when you generate code.
//That makes you set different factory method for each database that has different schema.
sp.TaskId = Guid.NewGuid();
//Set other properties...
var result = sp.ExecuteNonQuery();//Executed to db
您可以为每两个具有不同DatabaseKey。
默认数据库和事务混合时,事务具有优先级。
//Call once on application start what database you use...
DatabaseFactory.Current.SetCreateDatabaseMethod
("DbSharpSample", () => new HigLabo.Data.SqlServerDatabase("connection string to db1"));
var db2 = new HigLabo.Data.SqlServerDatabase("connection string");
var sp1 = new MyTaskTableInsert();
//...Set property of MyTaskTableInsert object
var result1 = sp1.ExecuteNonQuery();//Executed to db1
using (TransactionContext tx = new TransactionContext(db2))
{
tx.BeginTransaction(IsolationLevel.ReadCommitted);
var sp2 = new MyTaskTableInsert();
//...Set property of MyTaskTableInsert object
var result2 = sp2.ExecuteNonQuery(tx);
tx.CommitTransaction();
}
好的,这些功能是我首先要实现的。
我设计了类并实现了上述所有功能。类图如下所示:
每个类都有各自的职责。
- StoredProcedure类具有存储过程的常用操作。这个类的常用操作是GetStoredProcedureName, ExecuteNonQuery, CreateCommand,SetOutputParameterValue方法。
- MyTaskTableInsert类将从数据库中的存储过程模式生成。此类具有与存储过程的参数对应的属性。并且这个类有GetStoredProcedureName, CreateCommand, SetOutputParameterValue的具体实现。
- DatabaseRecord和StoredProcedureResultSet类是MyTaskTableSelectBy_TaskId.ResultSet类的基类。MyTaskTableSelectBy_TaskId.ResultSet类是一个POCO类,表示数据库中的表记录。(稍后,我将从表模式创建MyTaskTable.Record类,该类也从DatabaseRecord类继承。)
- StoredProcedureWithResultSet类继承自StoredProcedure类。该类增加了一些返回结果集的存储过程的常用操作。这个类的常用操作是GetDataTable, GetResultSets,EnumerateResultSets方法。
- StoredProcedureWithResultSet<T>类继承自StoredProcedureWithResultSet类。这个类的常用操作是CreateResultSet,SetResultSet方法。该类还添加了强类型方法以提高类型安全性。
- MyTaskTableSelectBy_TaskId将从数据库中的存储过程模式生成。此类具有与存储过程的参数对应的属性。这个类有GetStoredProcedureName, CreateCommand, SetOutputParameterValue, CreateResultSet,SetResultSet具体实现。
如您所见,每个类都有各自的职责。首先,我解释了StoredProcedure类和MyTaskTableInsert类。StoredProcedure类如下:
public abstract class StoredProcedure : INotifyPropertyChanged, ITransaction
{
public abstract DbCommand CreateCommand();
protected abstract void SetOutputParameterValue(DbCommand command);
public Int32 ExecuteNonQuery()
{
return this.ExecuteNonQuery(this.GetDatabase());
}
public Int32 ExecuteNonQuery(Database database)
{
if (database == null) throw new ArgumentNullException("database");
var affectedRecordCount = -1;
var previousState = database.ConnectionState;
try
{
var cm = CreateCommand();
affectedRecordCount = database.ExecuteCommand(cm);
this.SetOutputParameterValue(cm);
}
finally
{
if (previousState == ConnectionState.Closed &&
database.ConnectionState == ConnectionState.Open) { database.Close(); }
if (database.OnTransaction == false) { database.Dispose(); }
}
return affectedRecordCount;
}
//...abbreviated other elements
}
该StoredProcedure类具有生成的存储过程类的所有常见操作。您可以看到ExecuteNonQuery方法已定义。而且你还可以看到CreateCommand,SetOutputParameterValue抽象方法被定义了。当我实现一个生成器时,我唯一必须做的就是创建三个元素。
- 创建存储过程参数对应的属性
- 创建CreateCommand方法
- 创建SetOutputParameterValue方法
MyTaskTableInsert类将如下生成:
public partial class MyTaskTableInsert : StoredProcedure
{
private Guid? _TaskId;
private String _Title = "";
//...abbreviated other field
public Guid? TaskId
{
get
{
return _TaskId;
}
set
{
this.SetPropertyValue
(ref _TaskId, value, this.GetPropertyChangedEventHandler());
}
}
//...abbreviated other properties
public override DbCommand CreateCommand()
{
var db = new SqlServerDatabase("");
var cm = db.CreateCommand();
cm.CommandType = CommandType.StoredProcedure;
cm.CommandText = "MyTaskTableInsert";
DbParameter p = null;
p = db.CreateParameter("@TaskId", SqlDbType.UniqueIdentifier, 0, 0);
p.SourceColumn = p.ParameterName;
p.Direction = ParameterDirection.Input;
p.Size = 16;
p.Value = this.TaskId;
cm.Parameters.Add(p);
//...abbreviated other parameter
for (int i = 0; i < cm.Parameters.Count; i++)
{
if (cm.Parameters[i].Value == null) cm.Parameters[i].Value = DBNull.Value;
}
return cm;
}
protected override void SetOutputParameterValue(DbCommand command)
{
//Set property value from command object if output parameter exists.
}
//...abbreviated other elements
}
GetPropertyChangedEventHandler返回在基类(StoredProcedure类)中定义的PropertyChangedEventHandler。SetPropertyValue是在HigLabo.Core.dll的INotifyPropertyChangedExtensions类中定义的扩展方法。SetPropertyValue方法的内部是:
public static void SetPropertyValue<T, TProperty>
(this T obj, ref TProperty field, TProperty value
, PropertyChangedEventHandler onPropertyChanged,
[CallerMemberName] String propertyName = "")
where T : INotifyPropertyChanged
{
if (Object.Equals(field, value) == true) return;
field = value;
var eh = onPropertyChanged;
if (eh != null)
{
eh(obj, new PropertyChangedEventArgs(propertyName));
}
}
简单地说,它验证对象相等,设置field为value,并触发PropertyChanged事件。
这些类设计创建最少的工作,因为所有常见的操作都已经在StoredProcedure类中定义。
接下来,我解释返回resultset和其他的StoredProcedureWithResultSet类。这是列表:
- MyTaskTableSelectBy_TaskId.ResultSet类
- GetDataTable方法
- GetResultSets方法
- EnumerateResultSets方法
该MyTaskTableSelectBy_TaskId.ResultSet类是表示数据库表记录的POCO对象。要将值映射到强类型POCO对象的属性,有一个限制,即无论何时执行存储过程,结果集都必须具有相同的架构。
这是一个生成的ResultSet类。
public partial class ResultSet : StoredProcedureResultSet
{
private Guid? _TaskId;
private String _Title = "";
//...abbreviated other field
public Guid? TaskId
{
get
{
return _TaskId;
}
set
{
this.SetPropertyValue
(ref _TaskId, value, this.GetPropertyChangedEventHandler());
}
}
public String Title
{
get
{
return _Title;
}
set
{
this.SetPropertyValue
(ref _Title, value, this.GetPropertyChangedEventHandler());
}
}
//...abbreviated other property
//...abbreviated other elements
}
我设计了StoredProcedureWithResultSet,StoredProcedureWithResultSet<T>类并实现了上述列表中的所有常见功能。这里有一StoredProcedureWithResultSet类。你可以看到定义了GetDataTable、GetResultSets、EnumerateResultSets方法。
public abstract class StoredProcedureWithResultSet : StoredProcedure
{
protected StoredProcedureWithResultSet()
{
}
protected abstract StoredProcedureResultSet CreateResultSets(IDataReader reader);
public List<StoredProcedureResultSet> GetResultSets()
{
return EnumerateResultSets().ToList();
}
public List<StoredProcedureResultSet> GetResultSets(Database database)
{
return EnumerateResultSets(database).ToList();
}
public IEnumerable<StoredProcedureResultSet> EnumerateResultSets()
{
return EnumerateResultSets(this.GetDatabase());
}
public IEnumerable<StoredProcedureResultSet> EnumerateResultSets(Database database)
{
if (database == null) throw new ArgumentNullException("database");
DbDataReader dr = null;
var previousState = database.ConnectionState;
try
{
var resultsets = new List<StoredProcedureResultSet>();
var cm = CreateCommand();
dr = database.ExecuteReader(cm);
while (dr.Read())
{
var rs = CreateResultSets(dr);
resultsets.Add(rs);
yield return rs;
}
dr.Close();
this.SetOutputParameterValue(cm);
}
finally
{
if (dr != null) { dr.Dispose(); }
if (previousState == ConnectionState.Closed &&
database.ConnectionState == ConnectionState.Open) { database.Close(); }
if (database.OnTransaction == false) { database.Dispose(); }
}
}
public DataTable GetDataTable()
{
return GetDataTable(this.GetDatabase());
}
public DataTable GetDataTable(Database database)
{
if (database == null) throw new ArgumentNullException("database");
try
{
var cm = CreateCommand();
var dt = database.GetDataTable(cm);
return dt;
}
finally
{
if (database.OnTransaction == false) { database.Dispose(); }
}
}
//...abbreviated other method
}
StoredProcedureWithResultSet<T>就像下面一样。您可以看到定义了更多强类型的方法。
public abstract class StoredProcedureWithResultSet<T> : StoredProcedureWithResultSet
where T : StoredProcedureResultSet, new()
{
protected abstract void SetResultSet(T resultSet, IDataReader reader);
public abstract T CreateResultSet();
protected override StoredProcedureResultSet CreateResultSets(IDataReader reader)
{
var rs = this.CreateResultSet();
SetResultSet(rs, reader);
return rs;
}
public new List<T> GetResultSets()
{
return EnumerateResultSets().ToList();
}
public new List<T> GetResultSets(Database database)
{
return EnumerateResultSets(database).ToList();
}
public new IEnumerable<T> EnumerateResultSets()
{
return base.EnumerateResultSets().Cast<T>();
}
public new IEnumerable<T> EnumerateResultSets(Database database)
{
return base.EnumerateResultSets(database).Cast<T>();
}
}
这种设计创建了最少的工作,因为所有常见的操作都已经在这些类中定义了。所以,我只在下面的列表中创建这些元素:
- 存储过程的字段和属性
- CreateCommand方法
- SetOutputParameterValue方法
- CreateResultSet方法
- SetResultSet方法
这是其中的一些代码:
public partial class MyTaskTableSelectBy_TaskId :
StoredProcedureWithResultSet<MyTaskTableSelectBy_TaskId.ResultSet>
{
private Guid? _TaskId;
public Guid? TaskId
{
get
{
return _TaskId;
}
set
{
this.SetPropertyValue
(ref _TaskId, value, this.GetPropertyChangedEventHandler());
}
}
public override DbCommand CreateCommand()
{
var db = new SqlServerDatabase("");
var cm = db.CreateCommand();
cm.CommandType = CommandType.StoredProcedure;
cm.CommandText = this.GetStoredProcedureName();
DbParameter p = null;
p = db.CreateParameter("@TaskId", SqlDbType.UniqueIdentifier, 0, 0);
p.SourceColumn = p.ParameterName;
p.Direction = ParameterDirection.Input;
p.Size = 16;
p.Value = this.TaskId;
cm.Parameters.Add(p);
for (int i = 0; i < cm.Parameters.Count; i++)
{
if (cm.Parameters[i].Value == null) cm.Parameters[i].Value = DBNull.Value;
}
return cm;
}
protected override void SetOutputParameterValue(DbCommand command)
{
}
public override MyTaskTableSelectBy_TaskId.ResultSet CreateResultSet()
{
return new ResultSet(this);
}
protected override void SetResultSet
(MyTaskTableSelectBy_TaskId.ResultSet resultSet, IDataReader reader)
{
var r = resultSet;
Int32 index = -1;
try
{
index += 1;
if (reader[index] != DBNull.Value) r.TaskId = reader.GetGuid(index);
index += 1;
if (reader[index] != DBNull.Value) r.Title = reader[index] as String;
index += 1;
if (reader[index] != DBNull.Value) r.Priority = reader.GetInt32(index);
index += 1;
if (reader[index] != DBNull.Value) r.State = reader[index] as String;
index += 1;
if (reader[index] != DBNull.Value) r.CreateTime = reader.GetDateTime(index);
index += 1;
if (reader[index] != DBNull.Value) r.ScheduleDate = reader.GetDateTime(index);
index += 1;
if (reader[index] != DBNull.Value) r.Detail = reader[index] as String;
index += 1;
if (reader[index] != DBNull.Value) r.TimestampColumn = reader[index] as Byte[];
}
catch (InvalidCastException ex)
{
throw new StoredProcedureSchemaMismatchedException(this, index, ex);
}
}
//...abbreviated other elements
}
如您所见,无论存储过程如何CreateResultSet都会生成相同的代码,并且在许多情况下SetOutputParameterValue是空的。它使我很容易实现生成器。在下一节中,我将解释从数据库获取模式数据并从模式生成C#代码的详细信息。
从数据库中读取模式
从上面的设计来看,当我生成一些方法的实现时,我必须得到模式数据。
- CreateCommand
- SetOutputParameterValue
- SetResultSet
我计划在Microsoft SqlServer中支持这些通用类型。
bigint
timestamp
bigint
binary
image
varbinary
bit
char
nchar
ntext
nvarchar
text
varchar
xml
datetime
smalldatetime
date
time
datetime2
decimal
money
smallmoney
float
int
real
uniqueidentifier
smallint
tinyint
datetimeoffset
variant
还支持这些类型特定的Microsoft SqlServer。
udt(SqlGeometry)
udt(SqlGeography)
udt(HierarchyId)
UserDefinedTable column
我还想支持类型为UserDefinedType的参数。虽然它不是关于类型,但我计划支持 C# Enum功能。以下是本节列出的所有内容。
- 泛型列类型(Bit、Int、NVarchar等)
- Udt(SqlGeometry, SqlGeography, HierarchyId)
- Enum
- UserTable
我通过对本地数据库执行DbSharp_SqlServer.sql文件(在HigLaboSample.Data.SqlServer.CSharp项目中)来准备数据库。
Usp_Structure存储过程的CreateCommand方法内部必须如下所示:
public override DbCommand CreateCommand()
{
var db = new SqlServerDatabase("");
var cm = db.CreateCommand();
cm.CommandType = CommandType.StoredProcedure;
cm.CommandText = this.GetStoredProcedureName();
DbParameter p = null;
//General parameter
p = db.CreateParameter("@BigIntColumn", SqlDbType.BigInt, 19, 0);
p.SourceColumn = p.ParameterName;
p.Direction = ParameterDirection.InputOutput;
p.Size = 8;
p.Value = this.BigIntColumn;
cm.Parameters.Add(p);
//UserDefinedTable column
p = db.CreateParameter("@StructuredColumn", SqlDbType.Structured, 0, 0);
p.SourceColumn = p.ParameterName;
p.Direction = ParameterDirection.Input;
p.Size = -1;
p.SetTypeName("MyTableType");
var dt = this.StructuredColumn.CreateDataTable();
foreach (var item in this.StructuredColumn.Records)
{
dt.Rows.Add(item.GetValues());
}
p.Value = dt;
cm.Parameters.Add(p);
for (int i = 0; i < cm.Parameters.Count; i++)
{
if (cm.Parameters[i].Value == null) cm.Parameters[i].Value = DBNull.Value;
}
return cm;
}
SetOutputParameterValue方法内部必须如下所示:
protected override void SetOutputParameterValue(DbCommand command)
{
var cm = command;
DbParameter p = null;
p = cm.Parameters[0] as DbParameter;
if (p.Value != DBNull.Value && p.Value != null) this.BigIntColumn = (Int64)p.Value;
//Other parameters...
}
SetResultSet方法内部必须如下所示:
protected override void SetResultSet
(AllDataTypeTableSelectBy_PrimaryKey.ResultSet resultSet, IDataReader reader)
{
var r = resultSet;
Int32 index = -1;
try
{
index += 1; r.PrimaryKeyColumn = reader.GetInt64(index);
index += 1; r.TimestampColumn = reader[index] as Byte[];
index += 1;
if (reader[index] != DBNull.Value) r.BigIntColumn = reader.GetInt64(index);
//Other parameters...
index += 1;
if (reader[index] != DBNull.Value)
r.SqlVariantColumn = reader[index] as Object;
index += 1;
if (reader[index] != DBNull.Value)
r.GeometryColumn = (Microsoft.SqlServer.Types.SqlGeometry)reader[index];
index += 1;
if (reader[index] != DBNull.Value)
r.GeographyColumn = (Microsoft.SqlServer.Types.SqlGeography)reader[index];
index += 1;
if (reader[index] != DBNull.Value)
r.HierarchyIDColumn = (Microsoft.SqlServer.Types.SqlHierarchyId)reader[index];
index += 1;
if (reader[index] != DBNull.Value)
r.EnumColumn = StoredProcedure.ToEnum<MyEnum>
(reader[index] as String) ?? r.EnumColumn;
index += 1;
r.NotNullBigIntColumn = reader.GetInt64(index);
index += 1;
r.NotNullBinaryColumn = reader[index] as Byte[];
//Other parameters...
}
catch (InvalidCastException ex)
{
throw new StoredProcedureSchemaMismatchedException(this, index, ex);
}
}
要创建这些方法,我必须从模式中获取这些信息:
- ColumnName
- Type
- Length
- Precision
- Scale
- IsOutput范围
- UserTableTypeName
- UdtTypeName
因此,我创建了一个T-Sql脚本文件来获取上述架构。
Select T01.name as StoredProcedureName
,T02.name as ParameterName
,CASE T03.is_table_type
When 1 Then 'structured'
Else
Case T03.is_assembly_type
When 1 Then 'udt'
Else
Case T03.name
When 'sql_variant' Then 'variant'
Else T03.name
End
End
End as ParameterType
,Case T02.max_length
When -1 Then -1
Else
Case T03.name
When 'nvarchar' Then T02.max_length / 2
When 'nchar' Then T02.max_length / 2
Else T02.max_length
End
End as ParameterLength
,T02.precision as ParameterPrecision
,T02.scale as ParameterScale
,T02.is_output as IsOutput
,T02.default_value as DefaultValue
,CASE T03.is_table_type
When 1 Then T03.name
Else ''
End as UserTableTypeName
,Case T03.is_assembly_type
When 1 Then T03.name
Else ''
End as UdtTypeName
From sys.procedures as T01
Inner Join sys.parameters as T02
ON T01.object_id = T02.object_id
Inner Join sys.types as T03
ON T02.user_type_id = T03.user_type_id
Where T01.name = 'MyStoredProcedureName'
Order By T02.parameter_id
结果如下:
- 创建获取模式数据的T-Sql脚本(MySql、Oracle、PostgreSql等)
- 列出我想要支持的所有类型(MySql、Oracle、PostgreSql等)
接下来,我必须为上面的每种类型创建这三个代码块:
- 创建SetOutputParameterValue方法代码块
- 创建CreateCommand方法代码块
- 创建SetResultSet方法代码块
例如,我针对NVarchar创建了它。
CreateCommand
p = db.CreateParameter("@NVarCharColumn", SqlDbType.NVarChar, 0, 0);
p.SourceColumn = p.ParameterName;
p.Direction = ParameterDirection.InputOutput;
p.Size = 100;
p.Value = this.NVarCharColumn;
cm.Parameters.Add(p);
SetResultSet
r.NVarCharColumn = reader[index] as String;
SetOutputParameterValue
this.NVarCharColumn = (String)p.Value;
再举一个针对Geometry的例子。
CreateCommand
p = db.CreateParameter("@GeometryColumn", SqlDbType.Udt, 0, 0);//SqlDbType is Udt
p.SourceColumn = p.ParameterName;
p.Direction = ParameterDirection.InputOutput;
p.Size = -1;
p.SetUdtTypeName("geometry");//Set UdtTypeName property.
p.Value = this.GeometryColumn;
cm.Parameters.Add(p);
SetResultSet
r.GeometryColumn = (Microsoft.SqlServer.Types.SqlGeometry)reader[index];
SetOutputParameterValue
this.GeometryColumn = (Microsoft.SqlServer.Types.SqlGeometry)p.Value;
我创建了针对Microsoft SqlServer和MySql的所有实现,但没有实现Oracle、PostgreSql。如果您是这些数据库的专家,请告诉我。
- 创建获取模式数据的T-Sql脚本
- 列出所有类型
- 创建CreateCommand方法代码块
- 创建SetResultSet方法代码块
- 创建SetOutputParameterValue方法代码块
如果您为Oracle、PostgreSql创建上述列表,我将不胜感激,我会将您的代码合并到我的库中。
枚举支持
什么是Enum支持?我设计Enum这样的支持:
- C#属性定义为Enum,您可以获得强类型的好处
- 通过调用Enum的ToString方法将Enum值作为String插入到数据库中
- Column类型必须是NVarchar,NChar或其他文本类型
这些Enum属性可防止您将无效值插入数据库。我必须在从Object创建Enum的StoredProcedure类中实现ToEnum方法。
public static T? ToEnum<T>(Object value)
where T : struct
{
if (value == null) return null;
if (typeof(T).IsEnum == false) throw new ArgumentException("T must be Enum type");
T result;
var tp = value.GetType();
if (tp == typeof(T)) return (T)value;
if (tp == typeof(String) && Enum.TryParse((String)value, true, out result))
{
return result;
}
throw new InvalidEnumDataException(typeof(T), value);
}
我为Enum创建了方法代码块。
CreateCommand
p = db.CreateParameter("@EnumColumn", SqlDbType.NVarChar, 0, 0);
p.SourceColumn = p.ParameterName;
p.Direction = ParameterDirection.Input;
p.Size = 20;
p.Value = this.EnumColumn.ToStringFromEnum();
cm.Parameters.Add(p);
SetResultSet
r.EnumColumn = StoredProcedure.ToEnum<MyEnum>(reader[index] as String) ?? r.EnumColumn;
SetOutputParameterValue
this.EnumColumn = StoredProcedure.ToEnum<MyEnum>(p.Value as String) ?? this.EnumColumn;
如果数据库中存在无效值,StoredProcedure.ToEnum<MyEnum> throw InvalidEnumDataException。
用户定义类型
在本节中,我将向您展示如何设计UserDefinedType类。您可以在DbSharp_SqlServer.sql文件的顶部(在HigLabo.DbSharp.CodeGenerator.Version1项目中)看到MyTableType。
Create Type MyTableType As Table
(BigIntColumn bigint not null
,BinaryColumn binary(100)
,ImageColumn image
,VarBinaryColumn varbinary(100)
,BitColumn bit
,CharColumn char(100)
,NCharColumn nchar(100)
,NTextColumn ntext
,NVarCharColumn nvarchar(100)
,TextColumn text
,VarCharColumn varchar(100)
,XmlColumn xml
,DateTimeColumn datetime
,SmallDateTimeColumn smalldatetime
,DateColumn date
,TimeColumn time
,DateTime2Column datetime2
,DecimalColumn decimal
,MoneyColumn money
,SmallMoneyColumn smallmoney
,FloatColumn float
,IntColumn int
,RealColumn real
,UniqueIdentifierColumn uniqueidentifier
,SmallIntColumn smallint
,TinyIntColumn tinyint
,DateTimeOffsetColumn datetimeoffset(7)
,EnumColumn nvarchar(20)
)
我不支持TimeStamp,Geometry,Geography,HierarchyId。由于错误,我无法支持SqlVariant。请参阅错误详细信息。
您可以使用自己的UserDefinedType作为存储过程的参数。这是MyTableType的使用。
Create Procedure Usp_Structure
(@BigIntColumn bigint out
,@StructuredColumn as MyTableType readonly
) As
您将DataTable作为UserDefinedType参数值传递给数据库。这意味着您可以将记录列表发送到数据库。我从调用者位置设计如何使用UserDefinedType。
var udt = new MyTableType();
var record = new MyTableType.Record();
record.BigIntColumn = 100;
//Set other property...
udt.Records.Add(record);
var sp = new Usp_Structure();
sp.StructuredColumn = udt;
//Set other property...
sp.ExecuteNonQuery();
我设计了以下四个类:
- UserDefinedTableType
- UserDefinedTableType<T>
- MyTableType
- MyTableType.Record
我设计知道参数模式的UserDefinedTableType。UserDefinedTableType有GetDataTable abstract方法。UserDefinedTableType<T>提供保持多记录的功能。MyTableType是从数据库模式生成的,并有GetDataTable.GetDataTable的具体实现创建DataTable完全相同的模式UserDefinedType在数据库上。MyTableType.Record是一个POCO对象,表示UserDefinedType的记录。这是一个关于UserDefinedType的类图。
public abstract class UserDefinedTableType
{
public abstract DataTable CreateDataTable();
}
public abstract class UserDefinedTableType<T> : UserDefinedTableType
where T : UserDefinedTableTypeRecord
{
private List<T> _Records = new List<T>();
public List<T> Records
{
get { return _Records; }
}
public DataTable CreateDataTable(IEnumerable<T> records)
{
var dt = this.CreateDataTable();
foreach (var item in records)
{
dt.Rows.Add(item.GetValues());
}
return dt;
}
}
并从数据库的UserDefinedType模式中生成一个MyTableType类。
public partial class MyTableType : UserDefinedTableType<MyTableType.Record>
{
public override DataTable CreateDataTable()
{
var dt = new DataTable();
dt.Columns.Add("BigIntColumn", typeof(Int64));
//abbreviated other code...
return dt;
}
public partial class Record : UserDefinedTableTypeRecord
{
private Int64 _BigIntColumn;
//abbreviated other field...
public Int64 BigIntColumn
{
get
{
return _BigIntColumn;
}
set
{
this.SetPropertyValue
(ref _BigIntColumn, value, this.GetPropertyChangedEventHandler());
}
}
//abbreviated other properties...
public Record()
{
}
public override Object[] GetValues()
{
Object[] oo = new Object[28];
oo[0] = this.BigIntColumn;
//abbreviated other...
return oo;
}
}
}
UserDefinedType仅用于存储过程的参数。所以,我必须创建一个CreateCommand,SetOutputParameter方法。
CreateCommand
p = db.CreateParameter("@StructuredColumn", SqlDbType.Structured, 0, 0);
p.SourceColumn = p.ParameterName;
p.Direction = ParameterDirection.Input;
p.Size = -1;
p.SetTypeName("MyTableType");
var dt = this.StructuredColumn.CreateDataTable();
foreach (var item in this.StructuredColumn.Records)
{
dt.Rows.Add(item.GetValues());
}
p.Value = dt;
cm.Parameters.Add(p);
您不能将UserDefinedType作为输出参数:
Create Procedure Usp_Structure
(@BigIntColumn bigint out
,@StructuredColumn as MyTableType out --Invalid!!!
) As
所以你不需要生成SetOutputParameterValue方法代码。并且您不能将UserDefinedType用作结果集列。所以你不需要生成GetResultSets方法代码。
现在您可以使用强类型UserDefinedType作为存储过程的参数。
来自数据库的多个结果集
您可以从数据库中获取多个结果集。例如,您创建一个如下所示的存储过程:
Create Procedure MultipleResultSets_Procedure
As
select * from Table1
select * from Table1
select * from Table1
您不能将UserDefinedType作为输出参数:
var sp = new MultipleResultSets_Procedure();
var rsl = sp.GetResultSetsList();
foreach (var item in rsl.ResultSet1List)
{
}
foreach (var item in rsl.ResultSet2List)
{
}
您可以通过DbSharpApplication将自己的名称分配给结果集。
异步API
您可以使用ExecuteNonQuery, GetResultSetsAsync,GetResultSetsListAsync方法进行异步调用。
var sp = new MyStoredProcedure();
var rs = await sp.GetResultSetsAsync();
foreach (var item in rs)
{
///Do something...
}
使用起来非常简单。
CRUD操作的设计API
在本节中,我将向您展示如何设计对数据库表的CRUD操作。我通过这个脚本为数据库创建了一个示例表。
Create Table IdentityTable
(IntColumn int not null IDENTITY(1,1)
,NVarCharColumn nvarchar(100)
)
ALTER TABLE [dbo].[IdentityTable] ADD CONSTRAINT [PK_IdentityTable]
PRIMARY KEY CLUSTERED ([IntColumn])
我从调用者的位置设计Table和Table.Record类关于获取如下数据:
var db = new HigLabo.Data.SqlServerDatabase("connection string");
var t = new IdentityTable();
var records = t.SelectAll(db);
//Or you can call like this by extension method of DatabaseExtensions class
//var records = db.SelectAll(t);
foreach(var record in recordList)
{
//Do something...
}
//Get record that has a value of IntColumn = 1
var record = t.SelectByPrimaryKey(db, 1);
我设计了Insert, Update,Delete如下所示:
var db = new HigLabo.Data.SqlServerDatabase("connection string");
var record = new IdentityTable.Record();
//Insert sample
//IntColumn's value is automatically inserted(Because IDENTITY(1,1)) on database.
//So, you don't have to set value.
record.IntColumn = null;
record.NVarCharColumn = "MyText";
var t = new IdentityTable();
t.Insert(db, record);
//You can get a IntColumn value that is created on database
//after Insert method is executed.
Console.WriteLine(record.IntColumn);
//Update sample
record.NVarCharColumn = "MyNewText";
t.Update(db, record);
//Delete sample
t.Delete(db, record);
您可以缩写数据库的参数。如果缩写,表使用按DatabaseFactory类设置的默认数据库(与StoredProcedure类相同)。您可以通过调用DatabaseFactory对象SetCreateDatabaseMethod来设置默认数据库。
DatabaseFactory.Current.SetCreateDatabaseMethod
("DbSharpSample", () => new HigLabo.Data.SqlServerDatabase("connection string to DB1"));
var t = new IdentityTable();
var record = new IdentityTable.Record();
record.IntColumn = null;
record.NVarCharColumn = "MyText";
t.Insert(record);//Inserted to DB1
我设计的事务特性与StoredProcedures相同。
var db = new HigLabo.Data.SqlServerDatabase("connection string");
using (DatabaseContext dc = new DatabaseContext(db))
{
dc.BeginTransaction(IsolationLevel.ReadCommitted);
var t = new IdentityTable();
for (int i = 0; i < 3; i++)
{
var record = new IdentityTable.Record();
//Set properties...
t.Insert(record);//Inserted on transaction
}
dc.CommitTransaction();
}
这就是我想从调用方做的所有事情。
设计表,记录类
我设计了6个类,以CRUD将operation.IdentityTable,IdentityTable.Record类生成。其他在HigLabo.DbSharp.dll中定义。
- ITable
- Table<T>
- CRUD的存储过程
- IdentityTable
- TableRecord
- IdentityTable.Record
ITable类提供了与TableRecord类一起进行常见CRUD操作的功能。Table<T>类提供具有强类型记录类的CRUD操作的实现。此类具有用于CRUD操作的SelectAll, SelectByPrimaryKey, Insert, Update,Delete方法。为每个CRUD操作Table<T>使用StoredProcedure类。模式编辑器将生成五个存储过程和StoredProcedure类。这五个存储过程是:
- IdentityTableSelectAll
- IdentityTableSelectByPrimaryKey
- IdentityTableInsert
- IdentityTableUpdate
- IdentityTableDelete
IdentityTable类将从数据库中的表模式生成。TableRecord类对每个表的记录类都有共同的实现。Record类是表示数据库中表记录的类。它是从表的模式生成的。这是一个简单的POCO类来保存记录数据。
类图在这里:
这种设计也让我做最少的工作,因为所有常见的操作都在这些类中定义。
从数据库中获取架构
我必须获取列的模式数据才能生成C#源代码。这是一个从Microsoft数据库中获取模式数据的脚本。
SELECT T01.TABLE_NAME AS TableName
,T01.COLUMN_NAME AS ColumnName
,CASE T03.COLUMN_NAME
When T01.COLUMN_NAME Then convert(bit, 1)
Else convert(bit, 0)
End As IsPrimaryKey
,CASE T06.is_table_type
When 1 Then 'structured'
Else
Case T06.is_assembly_type
When 1 Then 'udt'
Else
Case T01.DATA_TYPE
When 'sql_variant' Then 'variant'
Else T01.DATA_TYPE End
End
End As DbType
,T01.CHARACTER_MAXIMUM_LENGTH AS ColumnLength
,T01.NUMERIC_PRECISION AS ColumnPrecision
,IsNull(T01.NUMERIC_SCALE,T01.DATETIME_PRECISION) AS ColumnScale
,Case T01.IS_NULLABLE When 'YES' Then convert(bit, 1) Else convert(bit, 0) End As AllowNull
,convert(bit, COLUMNPROPERTY(OBJECT_ID(QUOTENAME(T01.TABLE_SCHEMA) + '.' + _
QUOTENAME(T01.TABLE_NAME)), T01.COLUMN_NAME, 'IsIdentity')) as IsIdentity
,convert(bit, COLUMNPROPERTY(OBJECT_ID(QUOTENAME(T01.TABLE_SCHEMA) + '.' + _
QUOTENAME(T01.TABLE_NAME)), T01.COLUMN_NAME, 'IsRowGuidCol')) as IsRowGuid
,CASE T06.is_table_type
When 1 Then T06.name
Else
Case T06.is_assembly_type
When 1 Then T06.name
Else ''
End
End as UdtTypeName
,'' as EnumValues
FROM INFORMATION_SCHEMA.COLUMNS AS T01
LEFT JOIN (
SELECT T02.CONSTRAINT_NAME
, T02.TABLE_NAME
, T02.COLUMN_NAME
FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE AS T02
LEFT JOIN sys.key_constraints AS S01
ON T02.CONSTRAINT_NAME = S01.name
WHERE S01.type = 'PK'
) AS T03
ON T01.TABLE_NAME = T03.TABLE_NAME
AND T01.COLUMN_NAME = T03.COLUMN_NAME
Inner Join sys.tables as T04
ON T01.TABLE_NAME = T04.name
Inner Join sys.columns as T05
ON T04.object_id = T05.object_id AND T01.COLUMN_NAME = T05.name
Inner Join sys.types as T06
ON T05.user_type_id = T06.user_type_id
WHERE T01.TABLE_NAME = '{0}'
ORDER BY T01.ORDINAL_POSITION
结果在IdentityTable这里。
我将在下一节解释如何实现这些类。
记录类特性和实现
有了这些模式信息,我就可以生成IdentityTable.Record类了。该类提供以下功能:
- 属性对应于表的列
- OldRecord属性、SetOldRecordProperty方法
- CompareAllColumn,IsChanged方法
- SetProperty方法
- ConstructorExecuted部分方法
- GetValue, GetValues, SetValue,SetValues方法
- CreateValueArray方法
- GetTableName,GetColumnCount方法
IdentityTable.Record具有对应于列的属性。
public String NVarCharColumn
{
get
{
return _NVarCharColumn;
}
set
{
this.SetPropertyValue(ref _NVarCharColumn, value,
this.GetPropertyChangedEventHandler());
}
}
在为上述OldRecord属性设置值之前,属性会保留以前的值。首先,这个属性是null,你必须在访问这个属性之前调用SetOldRecordProperty方法。您可以通过调用SetOldRecordProperty方法设置Record对象属性的相同值。
var r = new IdentityTable.Record();
//r.OldRecord is null
r.NVarCharColumn = "MyValue1";
r.SetOldRecordProperty();
//r.OldRecord is not null
Console.WriteLine((r.NVarCharColumn == r.OldRecord.NVarCharColumn).ToString());//True
稍后,我将解释在Table类的SelectByPrimaryKey,Update,Delete方法内使用的OldRecord属性。
CompareAllColumn方法是从这样的模式生成的:
public override Boolean CompareAllColumn(Record record)
{
if (record == null) throw new ArgumentNullException("record");
var r = record;
return Object.Equals(this.IntColumn, r.IntColumn) &&
Object.Equals(this.NVarCharColumn, r.NVarCharColumn);
}
如您所见,此方法比较所有属性并返回所有列具有相同值的结果。
该IsChanged方法将对象本身与OldRecord对象进行比较。该方法在TableRecord<T>类中定义,简单地调用CompareAllColumn方法。
public Boolean IsChanged()
{
if (this.OldRecord == null) throw new InvalidOperationException
("You must call SetOldRecordProperty method before call IsChanged method.");
return this.CompareAllColumn(this.OldRecord);
}
您可以通过调用此方法知道某些值是否已更改。
SetProperty方法是从这样的模式生成的:
public void SetProperty(IRecord record)
{
if (record == null) throw new ArgumentNullException("record");
var r = record;
this.IntColumn = r.IntColumn;
this.NVarCharColumn = r.NVarCharColumn;
}
您可以使用此方法轻松设置属性。请注意,IdentityTableSelectAll.ResultSet和IdentityTableSelectByPrimaryKey.ResultSet类实现IdentityTable.IRecord接口。您可以将这些类作为IRecord参数传递给此方法。
您可以看到在IdentityTable.Record类的构造函数中调用了ConstructorExecuted部分方法。
public Record()
{
ConstructorExecuted();
}
public Record(IRecord record)
{
this.SetProperty(record);
ConstructorExecuted();
}
partial void ConstructorExecuted();
您可以定义自己的构造函数来设置此记录的默认值。
GetValue,SetValue生成并且GetValues,SetValues方法在TableRecord<T>类中定义。通过这些方法,您可以使用CSV,DataTable和其他Object[]. GetValue方法返回一个值作为对象。
public override Object GetValue(Int32 index)
{
switch (index)
{
case 0: return this.IntColumn;
case 1: return this.NVarCharColumn;
}
throw new ArgumentOutOfRangeException();
}
如果该值可以转换为属性的类型,则SetValue方法将值设置为属性。并返回bool您是否成功为属性设置值或失败。
public override Boolean SetValue(Int32 index, Object value)
{
switch (index)
{
case 0:
if (value == null)
{
return false;
}
else
{
var newValue = TableRecord.TypeConverter.ToInt32(value);
if (newValue == null) return false;
this.IntColumn = newValue.Value;
return true;
}
case 1:
if (value == null)
{
this.NVarCharColumn = null;
return true;
}
else
{
var newValue = value as String;
if (newValue == null) return false;
this.NVarCharColumn = newValue;
return true;
}
}
throw new ArgumentOutOfRangeException("index", index, "index must be 0-1");
}
如您所见,IntColumn不可为空,NVarCharColumn可以为空。行为是不同的,这取决于您传递null时属性是否可以为空。如果不能为null的属性,则不设置值并返回false(case 0)。如果属性可以null,则属性设置为null并返回true(case 1)。
在TableRecord<T>中GetValues方法被定义,并将所有属性值作为Object[]返回。
public Object[] GetValues()
{
var count = this.GetColumnCount();
var oo = new Object[count];
for (int i = 0; i < count; i++)
{
oo[i] = this.GetValue(i);
}
return oo;
}
例如,您可以在处理CSV文件时使用此方法。
SetValues方法将所有值设置为属性。
public SetValueResult[] SetValues(params Object[] values)
{
var count = values.Length;
var bb = new SetValueResult[count];
for (int i = 0; i < count; i++)
{
if (values[i] == TableRecord.SkipSetValue)
{
bb[i] = SetValueResult.Skip;
continue;
}
if (this.SetValue(i, values[i]) == true)
{
bb[i] = SetValueResult.Success;
}
else
{
bb[i] = SetValueResult.Failure;
}
}
return bb;
}
您可以使用TableRecord.SkipSetValue跳过某些属性。此方法返回SetValueResult[]。SetValueResult是一个具有三个值Success, Skip, Failure的enum。
public enum SetValueResult
{
Success,
Skip,
Failure,
}
您可以通过调用该CreateValueArray方法轻松创建值。
public Object[] CreateValueArray()
{
return this.CreateValueArray(SkipSetValue);
}
public Object[] CreateValueArray(Object defaultValue)
{
var count = this.GetColumnCount();
var oo = new Object[count];
for (int i = 0; i < count; i++)
{
oo[i] = defaultValue;
}
return oo;
}
您可以使用此方法,如下所示:
var r = new IdentityTable.Record();
var oo = TableRecord.CreateValueArray();
oo[1] = "MyText1";
var results = r.SetValues(oo);
//Do something...
您可以轻松地设置来自DataTable或CSV文件等的所有值。
GetTableName方法返回此记录的TableName,并且GetColumnCount返回此表的列数。
表类特性和实现
我生成具有以下功能的表类:
- SelectAll方法
- SelectByPrimaryKey方法
- Insert方法
- Update方法
- Delete方法
- Save方法
- BulkCopy
SelectAll返回List<IdentityTable.Record>对象。您可以通过DatabaseFactory调用SetCreateDatabaseMethod方法来设置默认数据库。
DatabaseFactory.Current.SetCreateDatabaseMethod
("DbSharpSample", () => new SqlServerDatabase("Connection string"));
var t = new IdentityTable();
var records = t.SelectAll();
foreach(var record in records)
{
//Do something...
}
您可以使用其他数据库,如下所示:
var db = new SqlServerDatabase("Connection string");
var t = new IdentityTable();
var records = t.SelectAll(db);
foreach(var record in records)
{
//Do something...
}
SelectByPrimaryKey方法返回一个与您传递的主键值相同的IdentityTable.Record。如果您传递的值与任何记录都不匹配,则TableRecordNotFoundException被抛出。
var t = new IdentityTable();
var record = t.SelectByPrimaryKey(-123);//TableRecordNotFoundException will be thrown here.
如果你想得到null而不是TableRecordNotFoundException,你可以调用SelectByPrimaryKeyOrNull方法来实现它。
var t = new IdentityTable();
var record = t.SelectByPrimaryKeyOrNull(1);//TableRecordNotFoundException is not thrown
//record may be null
您可以通过调用Insert方法插入记录。
var t = new IdentityTable();
var record = new IdentityTable.Record();
//record.IntColumn is auto increment value.The value will be set on Database.
record.NVarCharColumn = "MyValue1";
t.Insert(record);
insert之后,您可以获得在数据库上创建的值,如下所示:
//Code to prepare to insert...
t.Insert(record);
Console.WriteLine(record.IntColumn);//Show new value of auto increment column
您可以通过调用Update方法更新记录。
var t = new IdentityTable();
var record = t.SelectByPrimaryKey(1);
record.NVarCharColumn = "MyValue1";
t.Update(record);
请注意,record.OldRecord.IntColumn被用于确定您将更新的记录。
您可以通过调用如下Delete方法删除:
var t = new IdentityTable();
var record = new IdentityTable.Record();
record.SetOldRecordProperty();
record.OldRecord.IntColumn = 1;
t.Delete(record);
作为Update方法,record.OldRecord.IntColumn用于确定要删除的记录。
所有这些方法都有一个重载来传递Database对象。
var db = new SqlServerDatabase("Connection string");
var t = new IdentityTable();
var records = t.SelectAll(db);
var record = t.SelectByPrimaryKey(db);
t.Insert(db, record);
t.Update(db, record);
t.Delete(db, record);
您可以使用Save方法一次执行Insert, Update,Delete。
List<ISaveMode> records = new List<ISaveMode>();
var r1 = new IdentityTable.Record();
r1.SaveMode = SaveMode.Insert;
//Set properties...
records.Add(r1);
var r2 = new IdentityTable.Record();
r2.SaveMode = SaveMode.Update;
//Set properties...
records.Add(r2);
var r3 = new IdentityTable.Record();
r3.SaveMode = SaveMode.Delete;
//Set properties...
records.Add(r3);
var t = new IdentityTable();
t.Save(records);
DataAdapter在Save方法内部使用。在我的粗略测试中,它比执行Insert, Update, Delete方法快。
使用SqlServer时可以使用Table类的BuldCopy扩展方法。
var records = new List<IdentityTable.Record>();
var r1 = new IdentityTable.Record();
//Set properties...
records.Add(r1);
var t = new IdentityTable();
t.BulkCopy(records);
您还可以使用这样的SqlServerDatabase类执行批量插入:
var records = new List<IdentityTable.Record>();
var r1 = new IdentityTable.Record();
//Set properties...
records.Add(r1);
var reader = new TableRecordReader(records);
var db = new SqlServerDatabase("Connection string");
db.BulkCopy(IdentityTable.Name, reader);
BulkCopy方法也比执行Insert方法快。
Indentity、RowGuid、Timestamp列
在本节中,我将解释有关Indentity, RowGuid,Timestamp列的规范。这些列具有以下列出的功能:
- 值在数据库上自动生成
- 我们想知道新分配的值
您不能更新Indentity,Timestamp列,但可以更新RowGuid列。因此,您不必在插入记录时为这些列设置值。
var sp = new IndentityInsert();
//sp.IntColumn = 1; //You don't have to set a value
sp.NVarCharColumn = "MyValue1";
sp.ExecuteNonQuery();
Console.WriteLine(sp.IntColumn);//You can get newly assigned values.
您也可以按Indentity.Record类进行。
var t = new Indentity();
var r = new Indentity.Record();
r.NVarCharColumn = "MyValue1";
t.Insert(r);
Console.WriteLine(r.IntColumn);//You can get newly assigned values
//after Insert method is executed.
深入了解DatabaseContext、DatabaseFactory、Database类
在本节中,我将解释DatabaseContext类的内部。您可以通过以下DatabaseContext方式管理事务:
var db = new HigLabo.Data.SqlServerDatabase("connection string");
DatabaseContext dc = new DatabaseContext(db, "Transaction1");
初始化DatabaseContext对象时,将实例分配给DatabaseContext._Contexts线程静态字段。
您可以看到DatabaseContext._Contexts Dictionary<String, DatabaseContext>是并标记为ThreadStaticAttribute。
[ThreadStatic]
private static Dictionary<String, DatabaseContext> _Contexts;
ThreadStatic是一个属性,指示此static字段对于每个线程都是唯一的。
您必须注意的一件事是,如果您的initialize _Contexts像下面这样字段,它将导致NullReferenceException。
//Caution!! This is invalid code!!
[ThreadStatic]
private static Dictionary<String, DatabaseContext> _Contexts =
new Dictionary<String, DatabaseContext>();
因为首先,您在“Thread1”上访问_Contexts,然后,您在“Thread2”上访问_Contexts字段。static字段的初始化只在"Thread1"上执行一次,_Contexts字段仍然在"Thread2"上是null和并且抛出NullReferenceException。
所以你必须实现如下所示的ThreadStatic字段。并使用Contexts属性来访问它。
[ThreadStatic]
private static Dictionary<String, DatabaseContext> _Contexts = null;
private static Dictionary<String, DatabaseContext> Contexts
{
get
{
if (_Contexts == null)
{
_Contexts = new Dictionary<String, DatabaseContext>();
}
return _Contexts;
}
}
并通过属性访问。
var contexts = DatabaseContext.Contexts;
那是关于ThreadStatic变量的模式。请注意,您不必使用lock语句,因为只有当前线程可以访问此变量。
接下来,我将解释DatabaseContext对象的生命周期。例如,您如下所示初始化DatabaseContext:
var db = new HigLabo.Data.SqlServerDatabase("connection string");
DatabaseContext dc = new DatabaseContext(db);
这个实例被使用Database对象和transactionKey分配给_Contexts字段。您可以在DatabaseContext类的构造函数中看到它。
public DatabaseContext(Database database)
{
this.Initialize(database, "", null);
}
public DatabaseContext(Database database, String transactionKey)
{
this.Initialize(database, transactionKey, null);
}
public DatabaseContext(Database database, String transactionKey,
IsolationLevel isolationLevel)
{
this.Initialize(database, transactionKey, isolationLevel);
}
private void Initialize(Database database, String transactionKey,
IsolationLevel? isolationLevel)
{
this.TransactionKey = transactionKey;
this.Database = database;
DatabaseContext.SetDatabaseContext(this.TransactionKey, this);
if (isolationLevel.HasValue == true)
{
this.BeginTransaction(isolationLevel.Value);
}
}
private static void SetDatabaseContext(String transactionKey, DatabaseContext database)
{
var dcs = DatabaseContext.Contexts;
if (dcs.ContainsKey(transactionKey) == true)
throw new TransactionKeyAlreadyUsedException();
dcs[transactionKey] = database;
}
当您调用DatabaseContext类的Dispose方法时,此实例将从_Contexts字段中删除。
public void Dispose()
{
Database db = this.Database;
var dcs = DatabaseContext.Contexts;
if (dcs.ContainsKey(this.TransactionKey) == true)
{
dcs[this.TransactionKey] = null;
}
db.Dispose();
}
因此,您的实例位于从实例化到调用Dispose方法的括号之间。
//dc is assigned to _Contexts field from this line
using (DatabaseContext dc = new DatabaseContext(db, "Transaction1"))
{
//Do something...
} //dc is removed from _Contexts on this line
这是使用transaction特性的示例代码:
var db = new HigLabo.Data.SqlServerDatabase("connection string");
using (DatabaseContext dc = new DatabaseContext(db, "Transaction1"))
{
dc.BeginTransaction(IsolationLevel.ReadCommitted);
for (int i = 0; i < 3; i++)
{
var sp = new MyTaskTableInsert();
sp.TransactionKey = "Transaction1";
//...Set property of MyTaskTableInsert object
var result = sp.ExecuteNonQuery();
}
dc.CommitTransaction();
}
这是ExecuteNonQuery方法内部的代码:
public Int32 ExecuteNonQuery()
{
return this.ExecuteNonQuery(this.GetDatabase());
}
public Int32 ExecuteNonQuery(Database database)
{
if (database == null) throw new ArgumentNullException("database");
var affectedRecordCount = -1;
var previousState = database.ConnectionState;
try
{
var cm = CreateCommand();
affectedRecordCount = database.ExecuteCommand(cm);
this.SetOutputParameterValue(cm);
}
finally
{
if (previousState == ConnectionState.Closed &&
database.ConnectionState == ConnectionState.Open) { database.Close(); }
if (previousState == ConnectionState.Closed &&
database.OnTransaction == false) { database.Dispose(); }
}
return affectedRecordCount;
}
如您所见,StoredProcedure类通过调用IDatabaseContext接口的GetDatabase扩展方法来获取Database对象。
这是一个GetDatabase方法代码:
public static Database GetDatabase(this IDatabaseContext context)
{
Database db = null;
var dc = DatabaseContext.GetDatabaseContext(context.TransactionKey);
if (db == null)
{
if (context.TransactionKey == "")
{
db = DatabaseFactory.Current.CreateDatabase(context.GetDatabaseKey());
}
else
{
throw new TransactionKeyNotFoundException();
}
}
else
{
return dc.Database;
}
return db;
}
IDatabaseContext.GetDatabase方法获取声明为来自DatabaseContext_Contexts static字段的dc的DatabaseContext实例。并在dc private属性中使用Database。请注意,您指定了一些值给TransactionKey并且没有发现DatabaseContext,TransactionKeyNotFoundException将被抛出。
如果您设置sp.TransactionKey为“Transaction2”,如下所示,TransactionKeyNotFoundException将被抛出。
var db = new HigLabo.Data.SqlServerDatabase("connection string");
using (DatabaseContext dc = new DatabaseContext(db, "Transaction1"))
{
dc.BeginTransaction(IsolationLevel.ReadCommitted);
for (int i = 0; i < 3; i++)
{
var sp = new MyTaskTableInsert();
sp.TransactionKey = "Transaction2";
//...Set property of MyTaskTableInsert object
var result = sp.ExecuteNonQuery();//Throw exception!!!
}
dc.CommitTransaction();
}
因此,请尝试在设置TransactionKey=""时从DatabaseFactory.Current.CreateDatabase方法中获取Database。
public static Database GetDatabase(this IDatabaseContext context)
{
Database db = null;
var dc = DatabaseContext.GetDatabaseContext(context.TransactionKey);
if (db == null)
{
if (context.TransactionKey == "")
{
db = DatabaseFactory.Current.CreateDatabase(context.GetDatabaseKey());
}
else
{
throw new TransactionKeyNotFoundException();
}
}
else
{
return dc.Database;
}
return db;
}
请注意,GetDatabaseKey将返回您在生成源代码时设置的值。
您可以通过调用DatabaseFactory.Current.SetCreateDatabaseMethod设置Database默认值。
DatabaseFactory.Current.SetCreateDatabaseMethod
("DbSharpSample", () => new SqlServerDatabase("Connection string"));
如果将Database对象作为方法参数传递,则使用它。
var db = new HigLabo.Data.SqlServerDatabase("connection string");
var sp = new MyTaskTableInsert();
//...Set property of MyTaskTableInsert object
var result = sp.ExecuteNonQuery(db);
在上面的示例中,在sp执行ExecuteNonQuery方法时使用db。这是有关这些规则的优先事项。
- Database作为方法参数传递的对象
- DatabaseContext如果TransactionKey匹配
- 由DatabaseFactory.Current.CreateDatabase方法创建的Database对象
结论
DbSharp是我的爱好库,所以其他库可能比DbSharp有很多有点。有很多库可以帮助您访问数据源。这是我发现的一个名为ORM、Micro-ORM、DAL生成器的列表。
您可以将这些库用作备用库。如果您使用业务应用程序,它可能会更好。
DbSharp的主要特点是:
- SqlServer、MySql支持(Oracle、PostgreSql以后会支持...)
- 事务透明
- Timestamp, 身份支持
- UserDefinedType, RowGuid, Geometry, Geography, HierarchyId(SqlServer)
- BulkInsert支持(SqlServer)
- Enum支持
- Set支持(MySql)
- 没有XML映射文件
- 没有性能损失
- 没有LINQ
如果您发现其他缺少的功能,请与我联系。我会和你一起改进DbSharp!
现在,您可以从GitHub获取所有源代码。
https://www.codeproject.com/Articles/776811/DbSharp-DAL-Generator-Tool-on-NET6