具有 LINQ 支持的数据库框架






4.40/5 (7投票s)
一个易于使用的框架,支持多层、用户类型和 LINQ。
引言
我想在本文中介绍我的数据库框架。我决定创建它是在一个朋友问我他会选择哪个对象关系映射框架时。我看到的那些都不令我满意,而且,我也想稍微学习一下 LINQ,并创建一个易于使用的东西。
我有自己的目标
- 让数据库成为代码的附属,而不是相反,并且还要让框架能够处理任何事务性数据库。所以,我创建我的类,然后可以从类生成数据库。
- 支持用户创建的类型。一般来说,我不用 decimal 来存储货币值。我使用一个结构体,它有基本的货币值验证并正确显示它们。对于在巴西使用的文档(CPF、CNPJ、RG),我编写了类来验证它们并正确显示它们。所以,我在类中使用这些类型。
- 真正实现多层,这样“用户界面程序”可以在一台计算机上运行,而业务规则可以运行在另一台计算机上。
- 使用很少的属性。它们用于标记主键、使数据库字段可以为空,以及指定数据库中字符串的长度。但是,我不希望属性无处不在。
- 支持 LINQ。现在这是必须的,但在远程处理时比较棘手,因为表达式是不可序列化的。
- 以最简单的方式实现引用的懒加载。
- 在函数调用中使用尽可能少的参数,但拥有触手可及的一切所需。
- 在使用模式上保持一致,并至少尝试避免“常见”错误。
好吧,有了这些目标,我必须做出一些决定,所以
- 我有一个远程框架,它可以保持对象状态,但只能处理接口。所以,任何可以从远程主机调用的东西都必须以接口开头。
- 数据库记录类型也是接口。为什么?因为框架会在运行时自动实现它们。这样,只读字段都会得到验证,而无需开发人员记住,并且懒加载也可以自动完成,而无需像
RecordReference<recordtype>
这样的特殊结构。 - 更新是通过克隆完成的。这样,在验证规则时,您可以检查实际对象和旧对象,所有对象都类型正确。无需通过名称测试。这也可以使重构更容易,并允许更新命令仅影响已更改的字段。
- ThreadScopes。实际连接、实际事务和实际“验证错误包”都使用 ThreadScopes。此外,还会创建一个特殊的连接对象,以便它创建带有活动事务的命令,以此来避免创建没有事务的命令。如果要在导入文件时处理,但又不希望“捕获”每个记录生成的异常来生成错误列表,则错误处理的 ThreadScope 非常有用。仅在 Thread-Scope 错误验证结束时抛出异常,如果您不清除它。
- 字段必须标记为
DatabaseNullable
才能在数据库中允许 null 值。即使是Nullable<int>
也必须标记为DatabaseNullable
才能接受 null。但是,这不是错误。数据库接口可以有一个Nullable<int>
作为一种说法,即int
未初始化,而不是说字段在数据库中必须是可空的。而且,默认情况下将所有内容设为必需比默认情况下将所有内容设为可空更容易看到错误。 - LINQ。要实现完整的 LINQ 支持确实很难。所以,我专注于我认为它最有用的方面
- 选择整个对象、部分对象(仅选择某些字段)和单个字段,字段可以是直接的,也可以是通过自动关系。
- Where 子句由“数据库属性”操作和值组成。
string.BeginsWith;
string.EndsWith;
string.Contains;
值可以是常量或局部变量,但不能是像 value * 2 这样的表达式。此外,属性可以来自关系。而且,由于没有 LIKE 操作符,我添加了对以下操作的支持
列表中的 Contains()
操作符以反向模式工作,因此 List.Contains(Property)
被转换为 DatabaseField IN (List values)
。
好吧,关于想法我说了太多了,让我们实际看看。
使用代码
首先,我们需要设置我们的项目。
- 创建一个新项目。
- 添加对 Pfz.dll 和 Pfz.Databasing.dll 的引用。
- 创建一个空数据库,并在 app.config 中为其创建一个连接字符串。
例如,我的包含
<configuration>
<connectionstrings>
<add name="Default"
connectionstring="DataBase=Pfz.Databasing;user id=sa;password=12345"
providername="System.Data.SqlClient" />
</connectionstrings>
</configuration>
using System.Reflection;
using Pfz.Databasing;
namespace FirstSample
{
[DatabasePersisted]
[AlternateKey("ByName", "Name")]
public interface FirstTable:
IRecord
{
[PrimaryKey]
int Id { get; set; }
[DatabaseLength(255)]
string Name { get; set; }
}
class Program
{
static void Main(string[] args)
{
DatabaseManager.Value = LocalDatabaseManager.Value;
LocalDatabaseManager.Value.AddDefaultDataTypeConverters();
DatabaseScriptGenerator generator = new DatabaseScriptGenerator();
// Will generate script for all the persisted
// records in the given assembly.
generator.GenerateFor(Assembly.GetEntryAssembly());
using(Record.CreateThreadConnection())
{
// Here a local connection works.
// It is not returned as LocalConnection as it
// will not be one over the network.
// Direct command execution is only available locally.
LocalDatabaseConnection connection =
(LocalDatabaseConnection)ThreadConnection.Value;
using(var command = connection.CreateCommand())
{
foreach(var sql in generator.AllScripts)
{
command.CommandText = sql;
command.ExecuteNonQuery();
}
}
}
}
}
}
需要理解的点
- 在记录声明中
IRecord
是Pfz.Databasing
支持的任何对象的基接口。[DatabasePersisted]
属性必须用于任何创建数据库表的接口。仅继承IRecord
并不足够,因为您可以为您的对象创建基接口。例如,我总是创建一个IIdRecord
接口,它有一个 ID 作为主键,而不是将属性放在我每个表上。- 持久化接口必须至少有一个
[PrimaryKey]
属性。 [AlternateKey]
顾名思义,会在数据库中创建一个备用键(唯一索引)。[DatabaseLength]
可以与字符串一起使用,以设置其在数据库中的varchar
表示的长度。- 在
DatabaseManager
初始化时 - 在此示例版本中,未提供
RemoteDatabaseManager
,但理念是DatabaseManager.Value
必须设置为LocalDatabaseManager
或RemoteDatabaseManager
。 AddDefaultDataTypeConverters
添加了能够将值转换到/从数据库并生成正确脚本的转换器,用于int
、long
、string
以及其他基本类型。它们不是默认添加的,因为您可能只想使用自己的 DataTypeConverters。如果您想为特定类型创建自己的类型转换器或支持具有不同命名约定的不同数据库,请参阅IDataTypeConverter
接口。- 在脚本生成器中
DatabaseScriptGenerator
非常简单,但仍然强大。它能够发现项目中的所有持久化类型,并生成创建表、备用键和外键的脚本。- 生成脚本后,您可以单独访问每个脚本,通过
CreateTableScripts
、AlternateKeyScripts
和ForeignKeyScripts
属性。AllScripts
属性最适合首次创建。
实际创建:好吧,转换为获取 LocalDatabaseConnection
是一个笨拙的做法,但管理器默认不公开所使用的连接类型,因为在远程处理时,它可能是另一种类型,无法直接创建命令。这里的理念是增加一个安全级别。但是,代码很简单。通过 SQL 字符串 foreach
并执行它们。将生成一个只有一个表的数据库。
在查看更复杂的对象和层之前,让我们看看如何插入、更新和删除记录。
using System;
using System.Linq;
using Pfz.Databasing;
namespace SecondSample
{
[DatabasePersisted]
[AlternateKey("ByName", "Name")]
public interface FirstTable:
IRecord
{
[PrimaryKey]
int Id { get; set; }
[DatabaseLength(255)]
string Name { get; set; }
}
class Program
{
static void Main(string[] args)
{
DatabaseManager.Value = LocalDatabaseManager.Value;
LocalDatabaseManager.Value.AddDefaultDataTypeConverters();
using(Record.CreateThreadConnection())
{
using(var transaction = new ThreadTransaction())
{
for (int i=0; i<100; i++)
{
FirstTable item = Record.Create<firsttable>();
item.Id = i;
item.Name = i.ToString();
item.Apply();
}
var linqQuery1 =
from
record1
in
Record.GetQuery<firsttable>()
where
record1.Name.StartsWith("5") ||
record1.Name.EndsWith("7") || record1.Id == 1
select
record1.Name;
Console.WriteLine("Executing LinqQuery1");
foreach(string name in linqQuery1)
Console.WriteLine(name);
var linqQuery2 =
from
record2
in
Record.GetQuery<firsttable>()
where
record2.Id > 50
select
record2;
Console.WriteLine();
Console.WriteLine();
Console.WriteLine("Executing LinqQuery2 and updating records.");
// ToList is called because, at least in SqlServer,
// we can't execute queries while
// a datareader is opened. And the ToList
// copies all of them to the memory and closes
// the record. In other situations
// a secondary connection can be opened, but that's
// not the idea here, as we are still inside one transaction.
foreach(var record in linqQuery2.ToList())
{
var updateRecord = record.CreateUpdateRecord();
updateRecord.Name = "Test " + updateRecord.Name;
updateRecord.Apply();
}
Console.WriteLine();
Console.WriteLine();
Console.WriteLine("Executing LinqQuery3 and deleting records.");
var linqQuery3 =
from
record2
in
Record.GetQuery<firsttable>()
where
record2.Id <= 50
select
record2;
foreach(var record in linqQuery3.ToList())
{
record.Delete();
record.Apply();
}
// if I don't commit, everything is rollbacked.
transaction.Commit();
}
}
}
}
}
我使用上一个示例中的同一个类创建了一个新的示例。因此,我们现在可以插入、选择、更新和删除。看看它有多简单?如果我不想,我不需要创建事务。但它在那里是为了展示它是如何工作的。好吧,有了这些示例,让我们来看看一些其他的功能、属性和类。
有三个属性在前一个示例中未使用
[DatabaseName]
- 允许您为数据库中创建的表或字段指定不同的名称。[DatabaseNullable]
- 告诉脚本生成器在数据库中创建允许 null 的字段。当然,如果与值类型一起使用,请像这样声明:int?
(或Nullable<int32>
)。[DatabasePrecisionAndScale]
- 告诉脚本生成器为 decimal 字段使用给定的精度和标度。
此外,还有一个 [DatabaseIgnored]
属性,它告诉框架忽略该属性作为数据库属性。但是,由于自动实现会因未实现的属性而失败,因此它留待将来使用。
好的。有了这些属性,您已经可以构建一个更好的示例了。但是,我还想展示关系在这个框架中是如何变得非常简单的,使用基接口有多么有用,以及展示一个层的使用,特别是 BusinessRulesTier
。
using System;
using System.Reflection;
using System.Threading;
using Pfz.Databasing;
using Pfz.Databasing.BusinessRules;
using Pfz.Databasing.Tiers;
namespace ThirdSample
{
[DatabasePersisted]
public interface IIdRecord:
IRecord
{
[PrimaryKey]
long Id { get; set; }
}
public interface INamedRecord:
IIdRecord
{
// If the length is not set, it defaults to 255.
string Name { get; set; }
}
[DatabasePersisted]
[AlternateKey("ByName", "Name")]
public interface Country:
INamedRecord
{
}
[DatabasePersisted]
[AlternateKey("ByCountryAndName",
"Country", "Name")]
public interface State:
INamedRecord
{
Country Country { get; set; }
}
[DatabasePersisted]
[AlternateKey("ByStateAndName",
"State", "Name")]
public interface City:
INamedRecord
{
State State { get; set; }
}
public sealed class AutoGenerateIdRule:
Rule<iidrecord>,
ICreateRule<iidrecord>
{
private long fGenerator = DateTime.Now.Ticks;
public override void Execute(BusinessRulesTier rulesTier,
IDatabaseConnection connection, IIdRecord record)
{
record.Id = Interlocked.Increment(ref fGenerator);
}
}
class Program
{
static void Main(string[] args)
{
DatabaseManager.Value = LocalDatabaseManager.Value;
LocalDatabaseManager.Value.AddDefaultDataTypeConverters();
DatabaseManager.AddTier<inheritancetier>();
DatabaseManager.AddTier<businessrulestier>();
var generator = new DatabaseScriptGenerator();
generator.GenerateFor(Assembly.GetEntryAssembly());
using(Record.CreateThreadConnection())
{
var connection = (LocalDatabaseConnection)ThreadConnection.Value;
using(var command = connection.CreateCommand())
{
foreach(string sql in generator.AllScripts)
{
command.CommandText = sql;
command.ExecuteNonQuery();
}
}
// I will not use transaction in this sample.
var country = Record.Create<country>();
country.Name = "Brasil";
country = country.Apply();
var state = Record.Create<state>();
state.Country = country;
state.Name = "Paraná";
state = state.Apply();
var city = Record.Create<city>();
city.State = state;
city.Name = "Curitiba";
city.Apply();
var query =
from
record
in
Record.GetQuery<city>()
where
record.State.Country.Name == "Brasil"
select
new {City = record.Name, State = record.State.Name,
Country = record.State.Country.Name};
foreach(var record in query)
Console.WriteLine(record);
var query2 =
from
record2
in
Record.GetQuery<inamedrecord>()
select
record2.Name;
Console.WriteLine();
Console.WriteLine("Showing all INamedRecord names found in database:");
foreach(var name in query2)
Console.WriteLine(name);
Console.ReadLine();
}
}
}
}
要看的内容
- 我创建了
IIdRecord
和INamedRecord
基接口。 - Country、State 和 City 都是
INamedRecord
并已持久化。 - 我创建了一个规则,使用
DateTime.Now
自动生成 ID,然后加一。这样,我避免了创建“复杂”或特定于数据库的生成器。规则只需要实现ICreateRule
,但继承Rule
类会为您实现非泛型方法,指向泛型方法。 - 在初始化期间,我添加了一个名为
InheritanceTier
的层,以及另一个名为BusinessTier
的层。此时,它们没有优先级,但正确的顺序是这样。没有BusinessTier
,AutoGenerateIdRule
将永远不会被调用。它们需要被添加,因为一些项目可能不想使用它,尤其因为它可以位于客户端、服务器,甚至是一个真正多层远程应用程序中的中间层。而且,最后一个查询使用了InheritanceTier
,因为我正在对任何INamedObject
进行选择。 - 我再次将创建脚本放在此代码中,没有使用事务,并创建了一些记录。看看创建关系以及在 *select* 和 *where* 子句中使用关系有多么容易?
好吧,就这些了。希望您喜欢并使用它。