65.9K
CodeProject 正在变化。 阅读更多。
Home

DbSharpApplication(.NET6上的DAL生成工具,并发布了.NET8的新版本)

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.98/5 (60投票s)

2014 年 5 月 22 日

CPOL

23分钟阅读

viewsIcon

159382

downloadIcon

402

一个 DAL 生成器,它生成 StoredProcedure 客户端类并创建 Table 和 Record 类。

新闻

2024年2月2日:我发布了.NET8的新版本。请查看这篇文章

2021年12月31日:我将所有源代码更新到.NET6。请查看!

请下载最新版本的_.zip_文件。这些天我一直在修复一些bug。

摘要

本文通过创建DAL生成器(不是ORM...)来轻松调用存储过程,向您讲授OOP、测试优先、SRP。我将向您展示其实现。作为起点,请注意我使用Visual Studio 2022和SQL Server。

DbSharp是一个DAL生成器。它生成一个`StoredProcedure`客户端类,使您能够轻松调用存储过程。DbSharp还创建`Table`和`Record`类,使您能够进行CRUD操作。

我们发布了.NET6版本到GitHub和Nuget。

目录

您可以使用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。

选择菜单栏->编辑->管理连接
添加您的数据库连接字符串。

选择菜单栏->编辑->读取数据库Schema

连接按钮,确保所有对象都已选中,然后按执行按钮。

`Table`、`StoredProcedure`、`UserDefinedTableType`已导入。
您可以通过单击`Table`选项卡查看已导入的`Table`。

您可以通过单击`StoredProcedure`选项卡查看已导入的`StoredProcedure`。

您可以通过单击`UserDefinedTableType`选项卡查看已导入的`UserDefinedTableType`。

当您导入表时,每个表会生成5个用于CRUD操作的`StoredProcedure`。

  • SelectAll
  • SelectByPrimaryKey
  • Insert
  • 更新
  • 删除

您可以通过单击`StoredProcedure`选项卡查看这些`StoredProcedure`。

您可以将`Enum`类型管理到列。将`EnumName=MyEnum`设置为`AllDataTypeTable`的`EnumColumn`、`NotNullEnumColumn`。

您可以确认`EnumName`已自动设置为生成的`StoredProcedure`。

您可以看到`Usp_SelectMultiTable`向客户端返回多个`ResultSets`。

您可以将这些类名从`ResultSetX`更改为您自己的名称。

通过菜单栏->文件->保存将这些schema保存为文件。您可以通过菜单栏->文件->打开文件加载您的文件。

现在,您可以通过菜单栏->编辑->生成C#代码从这些schema生成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
  • 字段
  • 访问修饰符
  • 方法访问修饰符
  • 字段修饰符
  • 构造函数修饰符
  • 构造函数
  • 方法修饰符
  • 方法参数
  • 方法
  • 属性体
  • 属性
  • 类修饰符
  • 接口属性
  • 接口方法
  • 接口
  • 命名空间
  • 源代码

`TypeName`涵盖了`Type`的名称,例如`Int32`、`Person`、`List`、`Dictionary>`。首先,我创建了一个关于`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生成器。或者您可以轻松地将DLL划分为_HigLabo.ClassSchema.dll_、_HigLabo.CSharpCodeGenerator.dll_、_HigLabo.HtmlGenerator.dll_。

StringBuilder与TextWriter

本节解释了`TextWriter`在性能改进方面优于`StringBuilder`。在开始创建其他元素(`Field`、`AccessModifier`等)之前,我更改了`CSharpSourceCodeGenerator`设计。将来,我将从`StoredProcedure`模式生成许多文件。

我还必须考虑性能、内存使用和文件I/O。`StringBuilder`比`String`好,但一旦调用`ToString`方法,`StringBuilder`就会将数据分配到堆内存。如果使用`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版本测试用例和代码生成器,我将不胜感激。

设计StoredProcedure类

在本章中,我将向您展示如何通过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`相比,它是强类型的。由于强类型属性,您可以获得IntelliSense的巨大帮助。

您可以将存储过程执行到多个数据库(具有相同Schema)中,如下所示

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`为每个两个数据库Schema生成代码。

当默认数据库和事务混合时,事务具有优先级。

//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`类将从数据库中的存储过程schema生成。此类的属性对应于存储过程的参数。此类的`GetStoredProcedureName`、`CreateCommand`、`SetOutputParameterValue`具有具体实现。
  • `DatabaseRecord`和`StoredProcedureResultSet`类是`MyTaskTableSelectBy_TaskId.ResultSet`类的基类。`MyTaskTableSelectBy_TaskId.ResultSet`类是一个POCO类,表示数据库中表的一条记录。(稍后,我将从表schema创建`MyTaskTable.Record`类,该类也继承自`DatabaseRecord`类。)
  • `StoredProcedureWithResultSet`类继承自`StoredProcedure`类。此此类添加了一些返回结果集的存储过程的通用操作。此类的通用操作是`GetDataTable`、`GetResultSets`、`EnumerateResultSets`方法。
  • `StoredProcedureWithResultSet`类继承自`StoredProcedureWithResultSet`类。此类的通用操作是`CreateResultSet`、`SetResultSet`方法。此此类还添加了强类型方法以提高类型安全性。
  • `MyTaskTableSelectBy_TaskId`将从数据库中的存储过程schema生成。此类的属性对应于存储过程的参数。此类的`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));
        }
    }

简单来说,它验证对象相等性,将`value`设置为`field`,并触发`PropertyChanged`事件。

这些类设计最大限度地减少了工作量,因为所有通用操作都已在`StoredProcedure`类中定义。

接下来,我将解释返回`resultset`等的`StoredProcedureWithResultSet`类。以下是列表

  • `MyTaskTableSelectBy_TaskId.ResultSet`类
  • `GetDataTable`方法
  • `GetResultSets`方法
  • `EnumerateResultSets`方法

`MyTaskTableSelectBy_TaskId.ResultSet`类是一个POCO对象,表示数据库表的一条记录。为了将值映射到强类型POCO对象的属性,存在一个限制,即无论何时执行存储过程,结果集都必须具有相同的schema。

这是生成的`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`类,并实现了上述列表中所有通用功能。这是`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`如下所示。您可以看到定义了更多强类型方法。

    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`在许多情况下都是空的。这使得我更容易实现生成器。在下一节中,我将详细解释如何从数据库获取schema数据并从schema生成C#代码。

从数据库读取Schema

根据以上设计,当生成一些方法的实现时,我必须获取schema数据。

  • 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)
  • 枚举
  • 用户表

我通过执行_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);
        }
    }

要创建这些方法,我必须从schema获取这些信息

  • ColumnName
  • 类型
  • 长度
  • 精度
  • Scale
  • `IsOutput`参数
  • UserTableTypeName
  • UdtTypeName

因此,我创建了一个T-Sql脚本文件来获取上述schema。

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

结果如下所示

我针对Microsoft SqlServer进行了工作,发现每个数据库需要做的所有事情如下所示

  • 创建获取schema数据的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。如果您是这些数据库的专家,请告诉我。

  • 创建获取Schema数据的T-Sql脚本
  • 列出所有类型
  • 创建`CreateCommand`方法代码块
  • 创建`SetResultSet`方法代码块
  • 创建`SetOutputParameterValue`方法代码块

如果您为Oracle、PostgreSql创建了上述列表,我将非常感激,我会将您的代码合并到我的库中。

枚举支持

什么是`Enum`支持?我这样设计`Enum`支持

  • C#属性被定义为`Enum`,您可以获得强类型的好处
  • 通过调用`Enum`的`ToString`方法,`Enum`值以`String`形式插入到数据库中
  • `Column`类型必须是`NVarchar`、`NChar`或其他文本类型

这些`Enum`属性可以防止您向数据库插入无效值。我必须在`StoredProcedure`类中实现`ToEnum`方法,该方法从`Object`创建`Enum`。

    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 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`。由于一个bug,我无法支持`SqlVariant`。查看bug详情

您可以将自己的`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

我设计了了解参数schema的`UserDefinedTableType`。`UserDefinedTableType`有一个`GetDataTable` `abstract`方法。`UserDefinedTableType`提供了保持多条记录的功能。`MyTableType`从数据库schema生成,并具有`GetDataTable`的具体实现。`GetDataTable`创建与数据库上`UserDefinedType`完全相同的schema的`DataTable`。`MyTableType.Record`是一个POCO对象,表示`UserDefinedType`的一条记录。
这是关于`UserDefinedType`的类图。

所有通用功能都在`UserDefinedTableType`、`UserDefinedTableType`类中定义。

    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`的schema生成`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();
    }

这就是我想从调用者端做的一切。

设计Table、Record类

我设计了6个类来执行CRUD操作。将生成`IdentityTable`、`IdentityTable.Record`类。其他类定义在_HigLabo.DbSharp.dll_中。

  • ITable
  • Table<T>
  • CRUD的存储过程
  • IdentityTable
  • TableRecord
  • IdentityTable.Record

`ITable`类提供了与`TableRecord`类进行通用CRUD操作的功能。`Table`类提供了与强类型记录类进行CRUD操作的实现。此类具有用于CRUD操作的`SelectAll`、`SelectByPrimaryKey`、`Insert`、`Update`、`Delete`方法。`Table`为每个CRUD操作使用`StoredProcedure`类。通过schema编辑器将生成五个存储过程和`StoredProcedure`类。这五个存储过程是

  • IdentityTableSelectAll
  • IdentityTableSelectByPrimaryKey
  • IdentityTableInsert
  • IdentityTableUpdate
  • IdentityTableDelete

`IdentityTable`类将从数据库中的表schema生成。`TableRecord`类具有每个表的记录类的通用实现。`Record`类是一个表示数据库中表记录的类。它从表schema生成。它是一个简单的POCO类,用于保存记录数据。

类图在此

这种设计也让我做了最少的工作,因为所有通用操作都已在这些类中定义。

从数据库获取Schema

我必须获取列的schema数据才能生成C#源代码。这是一个从Microsoft数据库获取schema数据的脚本。

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`的结果在此。

我将在下一节解释如何实现这些类。

Record类特性与实现

有了这些schema信息,我就可以生成`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

稍后,我将解释`OldRecord`属性,它用于`Table`类的`SelectByPrimaryKey`、`Update`、`Delete`方法内部。

`CompareAllColumn`方法从schema生成,如下所示

    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`类中定义,并且只是简单地调用`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`方法从schema生成,如下所示

    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`参数传递给此方法。

您可以看到`ConstructorExecuted`分部方法在`IdentityTable.Record`类的构造函数中被调用。

    public Record()
    {
        ConstructorExecuted();
    }
    public Record(IRecord record)
    {
        this.SetProperty(record);
        ConstructorExecuted();
    }
    partial void ConstructorExecuted();

您可以定义自己的构造函数,为这条记录设置默认值。

`GetValue`、`SetValue`被生成,`GetValues`、`SetValues`方法在`TableRecord`类中定义。通过这些方法,您可以处理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`时,行为会有所不同,这取决于属性是否可为空。如果属性不可为空,则不设置值并返回`false`(情况`0`)。如果属性可为空,则将`null`设置为属性并返回`true`(情况1)。

`GetValues`方法在`TableRecord`中定义,并返回所有属性的值作为`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`是一个`enum`,它有三个值`Success`、`Skip`、`Failure`。

    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`返回此表的列数。

Table类特性与实现

我使用以下功能生成表类

  • `SelectAll`方法
  • `SelectByPrimaryKey`方法
  • `Insert`方法
  • `Update`方法
  • `Delete`方法
  • `Save`方法
  • BulkCopy

`SelectAll`返回`List`对象。您可以通过调用`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);

插入后,您可以获取在数据库中创建的值,如下所示

    //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`类的`BulkCopy`扩展方法。

    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`方法更快。

Identity、RowGuid、Timestamp列

在本节中,我将解释`Identity`、`RowGuid`、`Timestamp`列的规范。这些列具有以下列出的功能

  • 值在数据库中自动生成
  • 我们想知道新分配的值

您不能更新`Identity`、`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.

您也可以通过`Identity.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`,并被标记为`ThreadStaticAttribute`。

    [ThreadStatic]
    private static Dictionary<String, DatabaseContext> _Contexts;

`ThreadStatic`是一个属性,表示此`static`字段对于每个线程都是唯一的。

您必须小心的一点是,如果您像下面这样初始化`_Contexts`字段,它将导致`NullReferenceException`。

    //Caution!! This is invalid code!!
    [ThreadStatic]
    private static Dictionary<String, DatabaseContext> _Contexts = 
                                   new Dictionary<String, DatabaseContext>();

因为首先,您在“`Thread1`”上访问`_Contexts`,接下来,您在“`Thread2`”上访问`_Contexts`字段。`static`字段的初始化只在“`Thread1`”上执行一次,“`Thread2`”上的`_Contexts`字段仍然是`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`对象
  • 如果`TransactionKey`匹配,则为`DatabaseContext`
  • 由`DatabaseFactory.Current.CreateDatabase`方法创建的`Database`对象

结论

`DbSharp`是我的业余爱好库,所以其他库可能比`DbSharp`有更多优越之处。有许多库可以帮助您访问数据源。以下是我找到的被称为ORM、微ORM、DAL生成器的列表。

您可以将这些库用作替代品。如果您从事业务应用程序开发,这可能会更好。

`DbSharp`的主要功能是

  • 支持SqlServer、MySql(将来会支持Oracle、PostgreSql...)
  • 事务透明
  • 支持`Timestamp`、Identity
  • `UserDefinedType`、`RowGuid`、`Geometry`、`Geography`、`HierarchyId`(SqlServer)
  • 支持`BulkInsert`(SqlServer)
  • 支持`Enum`
  • 支持`Set`(MySql)
  • 无XML映射文件
  • 无性能损失
  • 无LINQ

如果您发现其他缺失的功能,请联系我。我将与您一起改进`DbSharp`!
现在,您可以从GitHub获取所有源代码。

历史

  • 2014年5月22日:初始版本
  • 2021年12月29日:所有源代码更新至.NET6
© . All rights reserved.