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

Signum Framework 教程 第二部分 – Southwind 逻辑

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.45/5 (6投票s)

2011年7月25日

LGPL3

25分钟阅读

viewsIcon

33080

downloadIcon

1102

在本部分中,我们将重点介绍编写业务逻辑、LINQ 查询并解释继承。

SignumFrameworkPart2/mapTicksLogic.png

 

Signum 框架教程 

内容 

引言 

Signum Framework 是一个用于开发以实体为中心的 N 层应用程序的新开源框架。其核心是 Signum.Engine,一个带有完整 LINQ 提供程序的 ORM,它在客户端-服务器(WPF 和 WCF)和 Web 应用程序(ASP.Net MVC)上都运行良好。

Signum Framework 的重点是简化可用于许多应用程序的可组合垂直模块的创建,并通过鼓励函数式编程和范围模式来促进简洁的代码。

如果您想了解 Signum Framework 的独特之处,请查看Signum Framework 原理

关于 Signum 框架 2.0

 

我们很高兴地宣布,我们终于发布了 Signum Framework 2.0。 

发布时间比预期要长,但因此也更具雄心。我们一直在日常内部使用我们的框架,我们认为这个版本已经完成,并将使所有敢于使用它的人感到高兴。

新版本侧重于不同的趋势

  • Signum.Web:基于 ASP.Net MVC 3.0 和 Razor,试图保持 Signum.Windows 相同的感觉和生产力,同时不限制 Web 的可能性(jQuery、Ajax、标记控制、友好 URL…)
  • 跟上技术:该框架现在仅在 .Net 4.0/ASP.Net MVC 3.0 上运行,并且代码片段和模板针对 Visual Studio 2010。
  • 改进几乎所有内容并修复错误:有关完整列表,请查看变更日志 http://www.signumframework.com/ChangeLog2.0.ashx

关于本系列

为了展示框架的功能并对架构有很好的理解,我们正在准备一系列教程,其中我们将在一个稳定的应用程序:Southwind 上进行工作。

Southwind 是 Northwind 的 Signum 版本,Northwind 是 Microsoft SQL Server 提供的著名示例数据库。

在本系列教程中,我们将创建整个应用程序,包括实体、业务逻辑、Windows (WPF) 和 Web (MVC) 用户界面、数据加载以及任何其他值得解释的方面。

上一个教程中,我们为 Southwind 应用程序创建了实体,我们了解了实体嵌入式实体MList<T>Lite<T>等。

在本教程中,我们将重点介绍编写业务逻辑以及如何与数据库交互。

业务逻辑

在 Signum 框架应用程序中,我们将业务逻辑理解为驻留在与实体不同的程序集中的代码片段,在本例中为Southwind.Logic,并在服务器上运行。

此程序集包含与数据库交互的代码: 

 

  • 业务规则 
  • 我们应用程序中定义的流程 
  • 将使用的查询 
  • 等等....

 

通过将所有这些代码放置在不同的程序集中,我们可以在不同的情况下使用它: 

 

  • 一个 Web 应用程序
  • 通过 WCF 服务运行的 Windows 应用程序
  • 具有管理目的的加载应用程序
  • 单元测试集合。

 

就像 Southwind.Entities 拥有所有将塑造我们数据的实体一样,Southwind.Logic 包含与几个高度内聚的实体(通常为 1 到 4 个实体)交互的类。我们称它们为“逻辑类”,它们负责

  • 将这些实体包含在模式中,以便应用程序知道这些实体需要在数据库中 
  • 注册将可供用户使用的查询(类似于视图)
  • 定义将处理这些实体的业务逻辑

与其他 ORM(如 NHibernate 或 Entity Framework)相反,Signum Framework 没有会话、数据上下文或工作单元模式的任何其他名称的概念。

相反,实体更改跟踪保存在实体本身内部,而其他相关信息(例如当前连接或事务,或已检索实体的缓存)则使用ThreadStatic变量保存在隐式上下文中。我们将此模式称为范围模式

使用此模式的主要好处(也是我们费心制作框架的主要原因)是,通过不传递代表我们数据库的对象,我们使代码与模式无关。这意味着处理数据库的代码将在包含类似表的其他模式中正常工作,从而可以重用垂直模块

此外,我们鼓励将逻辑类定义为静态类,这样它们通常没有状态,您无需在使用它们之前实例化任何东西。您还可以轻松地将它们上的方法定义为主要实体的扩展方法

EmployeeLogic:我们的第一个逻辑类

让我们从编写一个处理员工的类开始。我们将其命名为EmployeeLogic,它是一个新的静态类,如下所示

public static class EmployeeLogic 
{
    public static void Start(SchemaBuilder sb, DynamicQueryManager dqm) 
    { 
        if (sb.NotDefined(MethodInfo.GetCurrentMethod()))
        { 
            sb.Include<EmployeeDN>(); 
         } 
     } 
 } 

按照约定,我们创建一个接受SchemaBuilderDynamicQueryManagerStart静态方法。在此方法中,我们注册所有必要的东西,使 Employee 成为我们应用程序中一个功能齐全的模块。

首先要做的是检查该方法是否已调用,以避免两次注册模块。我们使用SchemaBuilder中方便的NotDefined方法。

在这种情况下,我们只将EmployeeDN包含在我们的模式中。正如我们在上一个教程中看到的,通过包含EmpoyeeDN,相关实体(在本例中为TerritoryDNRegionDN)会自动包含。

一些业务逻辑 

现在让我们在逻辑中创建一些方法。

第一个方法,用于在数据库中保存一个新员工,将如此简单

public static void Create(EmployeeDN employee) 
{ 
    if (!employee.IsNew)
        throw new ArgumentException("The employee should be new", "employee");
    employee.Save();
} 

请注意,每个IdentifiableEntity都有一个 IsNew 属性,指示实体是否已保存在数据库中。

此外,Save方法可能无法编译,因为它是在Signum.Engine命名空间中的静态类Database中定义的扩展方法。只需添加

using Signum.Engine; 

Database类包含与数据库交互的方法:SaveRetrieveDelete实体。有这些方法的泛型和无类型重载,以及处理单个实体或实体列表的重载。

public static class Database
{
   //Save
   public static T Save<T>(this T entity) where T : class, IIdentifiable;
   public static void SaveList<T>(this IEnumerable<T> entities) 
     where T : class, IIdentifiable;
   public static void SaveParams(params IIdentifiable[] entities);

   //Retrieve
   public static T Retrieve<T>(int id) where T : IdentifiableEntity;
   public static T Retrieve<T>(this Lite<T> lite) where T : class, IIdentifiable;   
   public static IdentifiableEntity Retrieve(Type type, int id);
   public static IdentifiableEntity Retrieve(Lite lite);
   
   public static T RetrieveAndForget<T>(this Lite<T> lite) 
     where T : class, IIdentifiable;
   public static IdentifiableEntity RetrieveAndForget(Lite lite);
   
   public static List<T> RetrieveAll<T>() where T : IdentifiableEntity;
   public static List<IdentifiableEntity> RetrieveAll(Type type);
   
   public static List<Lite<T>> RetrieveAllLite<T>() where T : IdentifiableEntity;
   public static List<Lite> RetrieveAllLite(Type type);
   
   public static List<T> RetrieveFromListOfLite<T>(this IEnumerable<Lite<T>> lites) 
     where T : class, IIdentifiable;
   public static List<IdentifiableEntity> RetrieveFromListOfLite(IEnumerable<Lite> lites);
   
   public static List<T> RetrieveList<T>(List<int> ids) where T : IdentifiableEntity;
   public static List<IdentifiableEntity> RetrieveList(Type type, List<int> ids);

   public static List<Lite<T>> RetrieveListLite<T>(List<int> ids) 
     where T : IdentifiableEntity;
   public static List<Lite> RetrieveListLite(Type type, List<int> ids);
   
   public static Lite<T> RetrieveLite<T>(int id) where T : IdentifiableEntity;
   public static Lite<T> RetrieveLite<T>(Type runtimeType, int id) 
     where T : class, IIdentifiable;
   public static Lite<T> RetrieveLite<T, RT>(int id)
     where T : class, IIdentifiable
     where RT : IdentifiableEntity, T;
   public static Lite RetrieveLite(Type type, int id);
   public static Lite RetrieveLite(Type type, Type runtimeType, int id);

   //Delete  
   public static void Delete<T>(int id) where T : IdentifiableEntity;
   public static void Delete<T>(this Lite<T> lite) where T : class, IIdentifiable;
   public static void Delete<T>(this T ident) where T : IdentifiableEntity;
   public static void Delete(Type type, int id);
   
   public static void DeleteList<T>(IList<int> ids) where T : IdentifiableEntity;
   public static void DeleteList<T>(IList<Lite<T>> collection) 
     where T : class, IIdentifiable;
   public static void DeleteList<T>(IList<T> collection) where T : IdentifiableEntity;
   public static void DeleteList(Type type, IList<int> ids);

   //Exist
   public static bool Exists<T>(int id) where T : IdentifiableEntity;
   public static bool Exists(Type type, int id);

   //ToStr
   public static Lite<T> FillToStr<T>(this Lite<T> lite) where T : class, IIdentifiable;
   public static string GetToStr<T>(int id) where T : IdentifiableEntity;
   public static Lite FillToStr(Lite lite);

   //Query
   public static IQueryable<T> Query<T>() where T : IdentifiableEntity;
   public static IQueryable<S> InDB<S>(this Lite<S> lite) where S : class, IIdentifiable;
   public static IQueryable<S> InDB<S>(this S entity) where S : IIdentifiable;
   public static IQueryable<MListElement<E, V>> MListQuery<E, V>(
     Expression<Func<E, MList<V>>> mlistProperty) 
     where E : IdentifiableEntity;
   public static IQueryable<T> View<T>() where T : IView;

   //UnsafeDelete
   public static int UnsafeDelete<T>(this IQueryable<T> query) 
     where T : IdentifiableEntity;
   public static int UnsafeDelete<E, V>(this IQueryable<MListElement<E, V>> query) 
     where E : IdentifiableEntity;

   //UnsafeUpdate
   public static int UnsafeUpdate<T>(this IQueryable<T> query, 
      Expression<Func<T, T>> updateConstructor) where T : IdentifiableEntity;
   public static int UnsafeUpdate<E, V>(this IQueryable<MListElement<E, V>> query, 
      Expression<Func<MListElement<E, V>, MListElement<E, V>>> updateConstructor) 
      where E : IdentifiableEntity;   
}

此外,由于Database中的大多数方法都是扩展方法(SaveSaveListDelete...),代码看起来简洁自然。但请记住,这只是一个编译技巧,实体是在不同的程序集中定义的,不依赖于引擎,也不依赖于数据库,因此实体可以在客户端计算机的 Windows 用户界面中使用。

让我们看另一个简单业务逻辑的例子

要按 Id 检索员工,请写入 

public static EmployeeDN Retrieve(int id) 
{ 
    Database.Retrieve<EmployeeDN>(id)
} 

要从数据库中删除实体,只需写入

public static void Remove(EmployeeDN employee) 
{ 
    employee.Delete();
} 

LINQ to Signum

Linq to Signum 是 Linq 提供程序的健壮而完整的实现,它与框架理念完美融合。您可以期望它拥有 Linq to Sql 目前所拥有的一切(SelectWhere、隐式和显式JoinGroupByOrderBySkipTakeSingle...)以及一些我们稍后将看到的新技巧。

我们可以在许多不同的情况下使用 Linq to Signum,通常在您的业务逻辑或加载应用程序中,或者您可以通过DynamicQueryManager向最终用户公开开放查询。

让我们创建一个稍微复杂一点的查询,例如,一个返回向当前员工汇报的 N 个最佳员工的方法。

public static List<Lite<EmployeeDN>> TopEmployees(int num)
{
    return (from e in Database.Query<EmployeeDN>()
            where e.ReportsTo.RefersTo(EmployeeDN.Current)
            orderby Database.Query<OrderDN>().Count(a => a.Employee == e.ToLite())
            select e.ToLite()).Take(num).ToList();
}   

正如您所看到的,语法感觉非常自然,与其他 Linq 提供程序相似,但有一些细微的差异

  • 我们没有表示数据库的显式上下文,每个表都有一个属性,相反,您必须使用Database.Query<T>()静态方法来访问实体表。它稍微长一点,但有利于代码重用,因为您的代码不会附加到特定的数据库模式。
  • 我们使用Lite<T>来表示延迟关系,通常是实体的身份信息。为了比较Lite<T>与实体,您可以使用RefersTo()ToLite()方法(也适用于不同静态类型的 lite 的转换)。

如果您已经了解其他IQueryable提供程序,考虑到这两个小差异,您应该已经能够编写相当复杂的查询了。

LINQ 提供程序如何工作? 

Linq 是 Microsoft 赋予我们的奇妙技术。它使查询任何数据源变得绝对相似且更具表现力,但抽象性很强,有时开发人员很难编写高效的代码。

我想深入探讨一些影响每个 Linq-Sql 提供程序但未正确解释的问题。

作为翻译过程的一部分,您的查询基本上分为 3 个部分。

  • 应在 C# 中评估并用作 SqlParameters 的常量子表达式
  • 表示您查询的 SQL 字符串
  • 将每个 DataRow 转换为结果对象的 lambda 表达式。

例如,在我们上次查询中,它将大致拆分为这样

(from e in Database.Query<EmployeeDN>()
where e.ReportsTo == EmployeeDN.Current.ToLite()
orderby Database.Query<OrderDN>().Count(a => a.Employee == e.ToLite())
select e.ToLite()).Take(num).ToList()
 

了解这一点很重要,因为如果绿色或蓝色代码存在错误,那么它看起来就像 Linq 提供程序中出现异常,但实际上是您的代码中。

此外,编写 Linq 提供程序是一项相当复杂的任务,除了实现所有 Linq 运算符的转换外,还需要解决一些难题,其中一些应由开发人员考虑

  • 处理可空性不匹配(C# 中显式,SQL 中隐式)。有时对于值类型,需要转换为Nullable<T>。 
    Database.Query<EmployeeDN>().Select(e => (int?)e.ReportsTo.Id)
  • 处理布尔表达式(SQL 没有布尔表达式,只有位值和条件)。这些转换使得一些 SQL 查询很笨拙。
    Database.Query<EmployeeDN>().Where(e => true)
    .Select(e => new { IsId2 = e.Id == 2})
  • 在评估先前的常量布尔子表达式后,已知无用的子表达式的评估和翻译被短路。 
    Database.Query<EmployeeDN>().Where(e => name != null ? e.Name == name: true)
    //Or 
    Database.Query<EmployeeDN>().Where(e => name == null || e.Name == name) 
  • 当结果集中每行都有子集合时,会自动进行客户端连接(没有 N+1 问题)。运行良好,但请注意。
    Database.Query<RegionDN>().Select(r => new { Region = r.ToLite(), r.Territories})
  • 如果结果集中有实体,请重构实体图而不会创建重复项。建议尽可能只检索Lite<T>,它更高效。 
    Database.Query<EmployeeDN>() 
    //there's only one copy of each employee, 
    //connecting ReportsTo reference the right way

用户的动态查询

DynamicQueryManager是一个与键“queryName”关联的查询存储库。这些查询是开放的,这意味着最终用户能够添加过滤器、更改排序甚至添加和删除列

此动态查询的主要用途是 Windows 和 Web 应用程序中的搜索对话框,但该概念足够通用,可供第三方使用者使用,例如用于将结果导出到 Excel 或在Signum Extensions中制作动态图形的工具。 

此外,当前唯一的实现基于Linq to Signum,利用了 Signum.Extensions 中实体的位置和授权系统,但也可以创建基于其他 Linq 提供程序或纯 Sql 视图的其他实现。

让我们付诸实践。在 Start 方法中,在将EmployeeDN包含在 Schema 中之后,我们将为 Employee 模块中的每个实体添加一个查询

public static void Start(SchemaBuilder sb, DynamicQueryManager dqm)
{
    if (sb.NotDefined(MethodInfo.GetCurrentMethod()))
    {
        sb.Include<EmployeeDN>();

        dqm[typeof(RegionDN)] = (from r in Database.Query<RegionDN>()
                                    select new
                                    {
                                        Entity = r.ToLite(),
                                        r.Id,
                                        r.Description,
                                    }).ToDynamic();

        dqm[typeof(TerritoryDN)] = (from t in Database.Query<TerritoryDN>()
                                    select new
                                    {
                                        Entity = t.ToLite(),
                                        t.Id,
                                        t.Description,
                                        Region = t.Region.ToLite()
                                    }).ToDynamic();
                
        dqm[typeof(EmployeeDN)] = (from e in Database.Query<EmployeeDN>()
                                    select new
                                    {
                                        Entity = e.ToLite(),
                                        e.Id,
                                        e.UserName,
                                        e.FirstName,
                                        e.LastName,
                                        e.BirthDate,
                                        e.Photo,
                                    }).ToDynamic();

    }
}
 

让我们分析一下代码。我们所做的是将每个查询与一个类型关联起来,在第一个案例中是typeof(RegionDN)

由于 queryName 是一个对象,我们可以使用任何System.Type作为名称。当查询与类型关联时,它将成为此类型的默认查询。这对于在用户界面中减少工作量很方便。

在右侧,我们有一个匿名类型的 IQueryable,它使用 ToDynamic 扩展方法转换为DynamicQuery<T>

为了创建动态查询,唯一必要的是提供一个IQueryable<T>,它包含一个类型为LiteEntity属性等其他属性。此值将是与每个记录关联的实体,并且不会显示给最终用户。 

您也可以将Lite<T>用于其他列,这样您将获得指向相关实体的链接,并且过滤器将具有自动完成功能。 

动态查询对用户开放更改: 

 

  • 筛选器:用户可以可视地聚合和删除由[列, 操作, 值]元组组成的筛选器。值和操作将对应于列的类型。 
    例如:Where [Territory.Region.Name, 等于, "UK"] 
  • 排序:用户可以按任意列(数字、字符串、实体等)升序或降序排序,并通过按住 Shift 键单击添加多个排序条件。
    例如:OrderBy [Territory.Region ASC], [Employee.Name DESC] 
  • 列:用户可以删除现有列并添加新列。新列可以是实体的任何字段,如果该字段具有另一个实体,则可以是该子实体的字段等等。将进行左外连接,因此行数不会改变。
     例如:添加列 [Territory.Region.Name], [Territory.Region.Id] 
  • 集合:对于集合字段,用户还可以根据集合中的元素过滤结果(通过AllAny)或将结果乘以集合中的元素(使用SelectMany实现)。在这种情况下,将弹出一条消息,提醒用户行数正在乘以。 
    例如:Where [Employee.Territories.Any, EqualsTo, (Territory,2)]
          添加列 [Employee.Territories.Element.Name] 
          OrderBy [Employee.Territories.Element.Id] 

 

所有这些功能使得动态查询对于高级最终用户来说是一项无价的技术,据我们所知,这是任何其他框架都无法实现的。但是,我们必须等待下一次教程才能看到它的实际运行。

ProductLogic:表达式属性

现在让我们创建一个ProductLogic静态类,其Start方法以 if NotDefined开头,然后我们将ProductDN包含在模式构建器中,最后为类别的管理添加一些查询

dqm[typeof(CategoryDN)] = (from s in Database.Query<CategoryDN>()
                           select new
                           {
                               Entity = s.ToLite(),
                               s.Id,
                               s.CategoryName,
                               s.Description,
                           }).ToDynamic(); 

供应商

dqm[typeof(SupplierDN)] = (from s in Database.Query<SupplierDN>()
               		       select new
                           {
                               Entity = s.ToLite(),
                               s.Id,
                               s.CompanyName,
                               s.ContactName,
                               s.Phone,
                               s.Fax,
                               s.HomePage,
                               s.Address
                           }).ToDynamic();

和产品

dqm[typeof(ProductDN)] = (from p in Database.Query<ProductDN>()
                          select new
                          {
                              Entity = p.ToLite(),
                              p.Id,
                              p.ProductName,
                              p.Supplier,
                              p.Category,
                              p.QuantityPerUnit,
                              p.UnitPrice,
                              p.UnitsInStock,
                              p.Discontinued
                          }).ToDynamic();

我们还为产品制作了一个查询,这样我们就无需一直过滤掉停产产品了

dqm[ProductQueries.Current] = (from p in Database.Query<ProductDN>()
                                where !p.Discontinued
                                select new
                                {
                                    Entity = p.ToLite(),
                                    p.Id,
                                    p.ProductName,
                                    p.Supplier,
                                    p.Category,
                                    p.QuantityPerUnit,
                                    p.UnitPrice,
                                    p.UnitsInStock,
                                }).ToDynamic();

请注意,在此第二个产品查询中,我们使用枚举而不是字符串作为 queryName。我们更喜欢枚举而不是字符串,因为它们是强类型且更易于本地化。 

我们不会创建任何处理实体的方法,用户界面的默认行为只会保存对象,这在这种情况下是正确的。

很简单,让我们把它变得复杂一点。 

假设对于管理员来说,拥有 ValueInStock 列(单价 * 库存单位)以避免在产品中累积过多资金会很有用。

我们可以在查询中创建该列,但这样我们就必须复制代码以在实体的用户界面、业务逻辑或某些报告中显示该值。

一个更优雅的解决方案是在产品实体本身中添加一个只读计算属性,如下所示

public class ProductDN : Entity
{
    (…)
        
    public decimal ValueInStock
    {
        get { return unitPrice * unitsInStock; }
    }

    (…)
}

不幸的是,此属性 getter 的主体将编译为 MSIL,因此 LINQ 提供程序将不知道 ValueInStock 的作用。我们希望将主体保留为表达式树,以便 LINQ 提供程序可以理解定义。

在 C# 团队添加此类支持之前,我们采用了以下约定:每当提供程序找不到属性或方法的翻译时,它会在同一类中查找同名但以 Expression 结尾的静态字段。此字段应包含具有相同输入和输出参数的 Lambda 表达式树 (Expression<T>),包括实例成员情况下的对象本身。

通过使用此功能,您可以向实体添加更多语义信息,并将业务逻辑分解为可重用的函数,这些函数可以被查询理解,从而使您的业务逻辑更简单且更易于维护。

Signum Framework 设置已安装两个代码片段以使用此功能。

expressionProperty [Tab] [Tab] ProductDN [Tab] decimal [Tab] ValueInStock  [Tab] p [Enter] 

也许我们需要添加以下命名空间

using System.Linq.Expressions;
using Signum.Utilities;  

最后,我们将使用我们的简单公式实现 lambda 的主体,但使用参数 p,因为该字段是静态的。结果应该如下所示

public class ProductDN : Entity
{
    (…)
        
    static Expression<Func<ProductDN, decimal>> ValueInStockExpression =
        p => p.unitPrice * p.unitsInStock;
    public decimal ValueInStock
    {
        get {  return ValueInStockExpression.Invoke(this); }
    }

    (…)
} 

现在 LINQ 提供程序理解此属性,因此我们可以在查询中使用它。

此外,如果我们在正常的内存中代码中使用该属性,Invoke 方法(在 Signum.Utilities 中定义)会编译、捕获并调用 lambda 表达式,因此我们只需编写一次代码。

如果需要查询的定义与代码不同,只需在 getter 中编写不同的代码即可。

此技术适用于属性和方法(甚至扩展方法),以及静态和实例成员,并且构成了扩展 LINQ 提供程序的最简单方法(还有两种以上方法)。

表达式树是如何生成的?

LINQ 语法使得查询内存中的对象与查询数据库非常相似,有时开发人员会感到困惑。原因是创建表达式树看起来与创建普通 lambda 表达式完全相同。 

表达式树是代码片段的运行时表示,可供库(如 LINQ 提供程序)使用。这些表达式树可以编译和执行,但编译的代码不能转换为表达式树。只有表达式树才能转换为 SQL。

创建此节点树的代码是使用Expression类的普通 C# 代码,可以由 C# 编译器自动生成,也可以手动编写。

目前,C# 编译器能够为类型推断为Expression<T>的 lambda 表达式生成表达式树, 其中T是一些委托类型(如Func, Action, EventHandler,...),并且只有当 lambda 表达式具有表达式体(而不是语句体)时。

但是,当我们调用Queryable静态类上的方法(Select、Where、GroupBy...)时,方法实际上是执行的(与普遍的看法相反),但它们唯一的目的是创建另一个IQueryable<T>,其中包含一个“扩展”先前IQueryable<T>表达式的表达式,并带有一个用于当前运算符的节点(代码自己编写!)。

例如,在我们的前一个查询示例中(不带查询理解)

Database.Query<EmployeeDN>()
.Where(e=>e.ReportsTo == EmployeeDN.Current.ToLite())
.OrderBy(e=>Database.Query<OrderDN>().Count(a => a.Employee == e.ToLite()))
.Select(e=>e.ToLite())
.Take(num)
.ToList();
  • C# 编译器创建的表达式
  • 运行时创建的表达式

这个微妙的区别在以下情况下很重要:假设我们创建了一个这样的扩展方法

public static IQueryable<ProductDN> AvailableOnly(this IQueryable<ProductDN> products)
{
    return products.Where(a => !a.Discontinued);
}  

只要它在“主查询路径”中使用,此代码就可以正常工作,因为它将被执行,但如果我们在 lambda 表达式中使用它(即:where 谓词),则代码不会执行,并且 LINQ 提供程序会找到 AvailableOnly 方法,但不知道它做什么。

使用 expressionMethods 我们可以规避这个问题

 static Expression<Func<IQueryable<ProductDN>, IQueryable<ProductDN>> AvailableOnlyExpression = 
  products => products.Where(a => !a.Discontinued); 
public static IQueryable<ProductDN AvailableOnly(this IQueryable<ProductDN> entity)
{
     return AvailableOnlyExpression.Invoke(entity);
}
 

注意:在更复杂的场景中,例如方法是泛型的或者有不同的重载时,我们可以使用MethodExpanderAttribute和 IMethodExpander 来教 LINQ 提供程序如何翻译未知方法。

CustomerLogic:继承和 ImplementedBy

每个 ORM 都必须以某种方式处理继承(每层次结构表、每子类表、每具体类表)。

Signum Framework 将每个具体类保存在自己的独立表中,并使用多态外键来建模继承。我们可以在实体中表示关系的任何字段(EntityLite<T>)上放置两个不同的属性

  • ImplementedBy: 创建一组互斥的外键以区分仅对应于实体一个字段的表。在只有少数不同实现时很有用。
  • ImplementedByAll: 创建两列(id 和 typeId),允许指向数据库中的任何实体,但没有参照完整性,并且 UI 和 LINQ 提供程序的支持较弱(没有自动联合)。

该解决方案比其他解决方案具有一些重要的优势

  • 每个实体只有一个TypeId,并且只存在于一个表中。例如:Id 为 4 的猫作为动物没有 Id
  • 数据库不需要知道类的所有层次结构(例如抽象类),只需要知道模式中包含的具体类。
  • 由于我们有一种方法可以覆盖属性——即使没有实体的控制——我们可以使用多态外键在我们的模块中添加扩展点。

让我们看一个继承的例子...

Northwind 是一个没有继承或多态概念的传统模式。在我们的模型中,我们将做的是假装 Southwind 现在拥有公司和个人作为客户,具有一些共同数据和一些不同数据,让我们回到 Southwind.Entities 并进行一些更改

  1. CustomerDN设为抽象,并创建两个新实体:PersonDNCompanyDN,两者都继承自CustomerDN
  2. companyNamecontactNamecontactTile移动到CompanyDN
  3. PersonDN添加一些属性:titlefirstNamelastNamedateOfBirth(将DateTimePrecissionValidator设置为 Days)。 
  4. 重写两个类中的ToString,返回CompanyDNCompanyName,以及PersonDN的名字和姓氏的连接。

如果我们此时尝试启动 Sothwind.Load 应用程序,我们将收到如下异常

SignumFrameworkPart2/Exception.gif

正如我们所看到的,我们不能在Schema中包含抽象类(或接口),只能包含可以实例化的具体类。

为了解决这个错误,我们需要向SchemaBuilder指示,与CustomerDN的关系将由PersonDNCompanyDN实现。如果我们可以控制该类,最简单的方法是在OrderDN的客户字段中添加一个ImplementedByAttribute: 

[ImplementedBy(typeof(CompanyDN), typeof(PersonDN))]
CustomerDN customer;
[NotNullValidator]
public CustomerDN Customer
{
    get { return customer; }
    set { Set(ref customer, value, () => Customer); }
}

同步

让我们再试一次,现在我们应该能够通过运行加载应用程序来同步模式。  

首先,同步器检测到CustomerDN表已被删除,并且已创建一些新表(CompanyDNPersonDN),并询问CustomerDN是否已重命名。我们目前没有数据,所以这并不重要,但如果有一些数据,我们可能希望将其移动到CompanyDN,所以让我们假装并回答CompanyDN。 

其次,它询问 OrderDN 中的idCutomer字段,现在我们将拥有idCustomer_CompanyDNidCustomer_PersonDN,如果我们要保留数据,我们必须选择第一个。

瞧!这是我们自动生成的同步脚本!

DROP INDEX OrderDN.FIX_OrderDN_idCustomer;

ALTER TABLE OrderDN DROP CONSTRAINT FK_OrderDN_idCustomer ;
EXEC SP_RENAME 'CustomerDN' , 'CompanyDN';
ALTER TABLE CompanyDN ALTER COLUMN ContactTitle NVARCHAR(10) NOT NULL;
ALTER TABLE EmployeeDN ADD UserName NVARCHAR(100) NOT NULL -- DEFAULT( );
ALTER TABLE EmployeeDN ADD PasswordHash NVARCHAR(200) NOT NULL -- DEFAULT( );

EXEC SP_RENAME 'OrderDN.idCustomer' , 'idCustomer_CompanyDN', 'COLUMN' ;
ALTER TABLE OrderDN ADD idCustomer_PersonDN INT NULL -- DEFAULT( );

CREATE TABLE PersonDN(
  Id INT IDENTITY NOT NULL PRIMARY KEY,
  ToStr NVARCHAR(200) NULL,
  Ticks BIGINT NOT NULL,
  Address_HasValue BIT NOT NULL,
  Address_Address NVARCHAR(60) NULL,
  Address_City NVARCHAR(15) NULL,
  Address_Region NVARCHAR(15) NULL,
  Address_PostalCode NVARCHAR(10) NULL,
  Address_Country NVARCHAR(15) NULL,
  Phone NVARCHAR(24) NOT NULL,
  Fax NVARCHAR(24) NOT NULL,
  FirstName NVARCHAR(40) NOT NULL,
  LastName NVARCHAR(40) NOT NULL,
  Title NVARCHAR(10) NOT NULL,
  DateOfBirth DATETIME NOT NULL
);

ALTER TABLE OrderDN ADD CONSTRAINT FK_OrderDN_idCustomer_CompanyDN FOREIGN KEY (idCustomer_CompanyDN) REFERENCES CompanyDN(Id);
ALTER TABLE OrderDN ADD CONSTRAINT FK_OrderDN_idCustomer_PersonDN FOREIGN KEY (idCustomer_PersonDN) REFERENCES PersonDN(Id);

CREATE INDEX FIX_OrderDN_idCustomer_CompanyDN ON OrderDN(idCustomer_CompanyDN);
CREATE INDEX FIX_OrderDN_idCustomer_PersonDN ON OrderDN(idCustomer_PersonDN);

UPDATE TypeDN SET --Customer
  ToStr = 'Company',
  FullClassName = 'Southwind.Entities.CompanyDN',
  TableName = 'CompanyDN',
  CleanName = 'Company',
  FriendlyName = 'Company'
 WHERE id = 3;

INSERT TypeDN (ToStr, FullClassName, TableName, CleanName, FriendlyName) 
 VALUES ('Person', 'Southwind.Entities.PersonDN', 'PersonDN', 'Person', 'Person');

如您所见,同步器能够

 

  • 创建、删除和重命名。 
  • 创建、删除和重命名,同时考虑类型和约束。 
  • 创建和删除外键。 
  • 创建和删除索引和索引视图。 
  • 在某些表(枚举等)中插入、更新或删除必要的记录。  

 

此功能使得团队协作变得非常愉快,因为每个成员无需维护一个包含必要更改的脚本(或花哨的迁移)并将其与其他脚本合并,这是一个非常容易出错的过程。只需获取最新版本,创建同步脚本,检查没有数据丢失,然后继续工作。  

如果向表中添加了一个非空字段,则会在语句中为您写入一个带注释的DEFAULT约束,以便您定义默认值。如果默认值取决于其他数据,您可以暂时将该字段设为可空,填充数据,然后再次生成同步脚本。  

在这种情况下,我们没有记录,所以我们可以直接删除DEFAULT约束并运行脚本。OrderDN表现在应该如下所示,请注意CustomerDN表已消失

SignumFrameworkPart2/diagramInheritance.gif

 

好的,让我们创建我们的CustomerLogic(实际上我们甚至还没有创建它,所有这些都是因为我们将OrderDN包含在MyEntityLogic中)。

像往常一样,一个带有Start方法的静态类,我们包含CompanyDNPersonDN并为它们添加一些默认查询。结果应该像这样

public static class CustomerLogic
{
    public static void Start(SchemaBuilder sb, DynamicQueryManager dqm)
    {
        if (sb.NotDefined(MethodInfo.GetCurrentMethod()))
        {
            sb.Include<PersonDN>();
            sb.Include<CompanyDN>();
                
            dqm[typeof(PersonDN)] = (from r in Database.Query<PersonDN>()
                                     select new
                                     {
                                         Entity = r.ToLite(),
                                         r.Id,
                                         r.FirstName,
                                         r.LastName,
                                         r.DateOfBirth,
                                         r.Phone, 
                                         r.Fax,
                                         r.Address,
                                     }).ToDynamic();

                dqm[typeof(CompanyDN)] = (from r in Database.Query<CompanyDN>()
                                          select new
                                          {
                                              Entity = r.ToLite(),
                                              r.Id,
                                              r.CompanyName,
                                              r.ContactName,
                                              r.ContactTitle,
                                              r.Phone, 
                                              r.Fax,
                                              r.Address,
                                          }).ToDynamic();
        }
    }
}

最后,如果我们无法控制OrderDN实体或CustomerDN实体(因为它们在共享库中实现),我们仍然可以在我们的Starter类(全局类)的开头在运行时覆盖属性,如下所示

sb.Settings.OverrideFieldAttributes((OrderDN o) => o.Customer, 
       new ImplementedByAttribute(typeof(CompanyDN), typeof(PersonDN))); 

OrderLogic:最后一步

我们的最后一步是将MyEntityLogic重命名为OrderLogic以匹配其当前的职责,并为其添加一个默认查询。

dqm[typeof(OrderDN)] = (from o in Database.Query<orderdn>()
                        select new
                        {
                             Entity = o.ToLite(),
                             o.Id,
                             Customer = o.Customer.ToLite(),
                             o.Employee,
                             o.OrderDate,
                             o.RequiredDate,
                             o.ShipAddress, 
                             o.ShipVia,
                        }).ToDynamic();
</orderdn>

我们还将创建一个显示每个订单(OrderLinesDN)内容的查询。同样,我们将使用枚举作为键。

dqm[OrderQueries.OrderLines] = (from o in Database.Query<OrderDN>()
                                from od in o.Details
                                select new
                                {
                                    Entity = o.ToLite(),
                                    o.Id,
                                    od.Product,
                                    od.Quantity,
                                    od.UnitPrice,
                                    od.Discount,
                                }).ToDynamic();

这些查询都很好,但是...计算每个OrderDetailSubTotalPrice是否会很有用? 

我们可以再次使用expressionProperty代码片段来实现,这样我们就可以在任何场景中都拥有该属性。

[Serializable]
public class OrderDetailsDN : EmbeddedEntity
{
    (…)

    static Expression<Func<OrderDetailsDN, decimal>> SubTotalPriceExpression =
        od => od.Quantity * od.UnitPrice * (decimal)(1 - od.Discount);
    public decimal SubTotalPrice
    {
        get{ return SubTotalPriceExpression.Invoke(this); }
    }

    (…)
}

我们也可以对OrderDN中的TotalPrice做同样的事情。

[Serializable]
public class OrderDN : Entity

{
    (…)

    static Expression<Func<OrderDN, decimal>> TotalPriceExpression =
        o => o.Details.Sum(od => od.SubTotalPrice);
    public decimal TotalPrice
    {
        get{ return TotalPriceExpression.Invoke(this); }
    }

    (…)
}

最后,让我们编写一些实际执行某些操作的业务逻辑。

假设每次在数据库中创建新的OrderDN时,都必须从库存中删除产品。如果用户界面尝试创建的订单产品数量超过可用数量,则应抛出异常以中止操作。

这段代码实现了这一点

public static OrderDN Create(OrderDN order)
{
    if (!order.IsNew)
        throw new ArgumentException("order should be new");

    using (Transaction tr = new Transaction())
    {
        foreach (var od in order.Details)
        {
            int updated = od.Product.InDB()
.Where(p => p.UnitsInStock >= od.Quantity)
.UnsafeUpdate<ProductDN>(p => new ProductDN
            	{
                  UnitsInStock = (short)(p.UnitsInStock - od.Quantity)
            	});

            if (updated != 1)
                throw new ApplicationException("There are not enought {0} in stock"
.Formato(od.Product)); 

        }

        order.Save(); 

        return tr.Commit(order); 
    }
}  

请注意,大部分主体都在一个事务中。 

Signum.EngineTransaction对象模仿TransactionScope语法,但它不会提升为分布式事务(也不需要 MSDTC)。 

另一个区别是,在嵌套事务的情况下,嵌套事务不会自行提交,从而使业务逻辑更容易组合。您可以通过强制事务独立或将其设为命名事务来更改此行为。  

在 foreach 循环内部,我们可以看到一个新的UnsafeUpdate方法。

UnsafeUpdateUnsafeDelete是一种修改和删除数据库记录的轻量级方式。它模仿SQL UPDATEDELETE语法,因此速度很快,但了解框架的所有约定和原语(EmbeddedEntitiesenumLiteImplementedBy…)。另一方面,它不执行任何验证,这就是为什么它被称为Unsafe

请注意,UnsafeUpdate接受一个Func<ProductDN, ProductDN>,此函数期望一个对象初始化表达式,该表达式创建一个新的ProductDN并设置要更新的属性,但它不会创建任何新对象!这只是一个语法技巧。

UnsafeDelete接受一个IQueryable<T>,其中T是一个具体的实体类型,因此它可以在任何查询之后使用,并且与 UnsafeUpdate 一样,返回受影响的列数。  

在这种情况下,我们正在使用另一个有用的新朋友,InDB。此方法从内存中的实体或Lite创建IQueryable<T>,并且等同于

 od.Product.InDB() //Equivalent to
 Database.Query<ProductDN>().Where(p => p == od.Product)
 

最后,UnsafeUpdate返回修改的行数。在这种情况下,InDB已经只选择了一个产品,所以如果Where过滤了产品并且我们没有更新任何行,我们知道我们没有足够的库存,我们可以抛出异常。

Starter 类:将所有内容整合在一起

最后一步是将所有这些模块添加到您的应用程序中。

我们已经看到每个逻辑模块都有一个Start方法,负责向模式添加必要的表,注册查询,挂接事件等等……

Starter类中还有一个全局Start方法,此方法负责通过调用每个模块上的Start方法来启动您将在应用程序中使用的所有模块。

public static class Starter
{
    public static void Start(string connectionString)
    {
        SchemaBuilder sb = new SchemaBuilder();
        DynamicQueryManager dqm = new DynamicQueryManager();
        sb.Schema.ForceCultureInfo = CultureInfo.InvariantCulture;
        ConnectionScope.Default = new Connection(connectionString, sb.Schema, dqm);

        EmployeeLogic.Start(sb, dqm);
        ProductLogic.Start(sb, dqm);
        CustomerLogic.Start(sb, dqm); 
        OrderLogic.Start(sb, dqm);
    }
}
   

Start方法被调用之后,您的应用程序范围就定义了,我们准备好生成数据库,同步或尝试初始化应用程序。

开始、初始化、运行!

虽然在投入生产时初始化应用程序很简单(只需使用Schema.Current.Initialize()初始化所有内容),但在其他主机应用程序中可能会变得有点棘手

 也许在运行加载应用程序时,您不想启动某些模块(例如未来 Signum Extensions 中可用的计划任务或后台进程)。 

此外,您经常希望在使用引擎加载某些依赖模块(例如:授权系统)之前保存一些基本实体(例如:匿名用户)。

对于这些场景,引擎有 5 个不同的初始化级别(级别 0 到级别 4),如果模块需要在运行应用程序之前调用一些代码(例如,填充一些缓存),它可以通过订阅正确的初始化级别(使用sb.Schema.Initializing[level] += myInitCode)来实现。

让我们了解不同应用程序主机中的初始化序列: 

SignumFrameworkPart2/initializationSequence.png

结论

在这篇长文中,我们学习了如何使用 Signum Framework 编写业务逻辑,例如

  • 以模块化方式创建我们自己的业务逻辑。 
  • 如何保存检索删除对象。
  • 使用 Linq to Signum 查询数据库。
  • 使用expressionProperty模板创建我们自己的启用数据库的计算属性。
  • 使用ImplementedBy表示类层次结构的多态关系。
  • 同步数据库。
  • 使用事务。 
  • 使用UnsafeUpdate快速更改数据库中的值,而无需任何验证仪式。
  • Start/Initialize序列如何工作

当然,关于如何使用 Signum.Framework 编写逻辑类,还有一些内容有待解释: 

  • 使用 EntityEvents 或覆盖实体本身的方法,在实体保存或检索之前获得控制权。
  • 挂钩到数据库的同步和生成以包含您自己的特定脚本。

而且,一旦 Signum.Extensions 发布,我们就可以看到授权模块如何保护数据库中的任何资源(类型、属性、查询……),以及操作、进程和计划任务模块如何简化业务逻辑的编写。

在下一个教程中,我们将使用 Southwind.Load 通过 Linq to Sql 和 Csv 文件从 Northwind 数据库移动数据。

希望很快我们就能看到一个带有按钮的窗口,它确实能做些什么 微笑 | <img src= " />

© . All rights reserved.