zoukankan      html  css  js  c++  java
  • OEA ORM 框架中的冗余属性设计

    OEA 框架提供了多种方式来优化分布式数据查询的性能,本篇将会说明如何以声明 OEA 冗余属性的方式,来实现轻量级的数据冗余,以减少关联查询次数及网络数据传输量,提高分布式应用程序性能。

    冗余属性功能说明


    OEA 冗余属性在框架层面提供了一种易用的机制,把指定冗余路径的关系对象中的属性值复制到本对象中,以解决关联查询、关联数据量等性能问题。应用开发人员只需要简单的定义一个冗余属性,而框架会自动完成对冗余属性的赋值、更新操作。

    ORM 中的 N+1 问题示例


    在进销存示例中,采购订单的列表界面中,每一行采购订单都要显示它对应的供应商名称:

    image

    在不作任何优化处理的情况下,每一行订单数据的这个供应商名称值都会从它的关系中加载,即:PurchaseOrder.Supplier.Name,这样造成 “N+1” 问题,即两行数据查询了三次,分别是:

    image

    这样,如果列表中有 10 条数据就会查询 11 次。

    编写冗余属性优化


    对于 N+1 问题,OEA 之前提供了《聚合 SQL》的方式,来实现生成 Left Outer Join 查询一个大表,并直接加载整个聚合对象及它对应的关系。而现在我将用冗余属性把供应商名称冗余到采购订单表 PurchaseOrder 上。

    目前,PurchaseOrder 采购订单表已经有了到供应商表(客户表) ClientInfo 的引用属性:

    image

    数据库结构如下:

    image

    那么,如果把它对应的供应商的名称冗余到 PurchaseOrder 表中呢?

    其实,只要使用 OEA CodeSnippet 提供的代码段模板在 PurchaseOrder 上定义一个冗余属性 SupplierName 就行了,模板如下:

    image

    代码段中,需要编写属性类型、属性名称以及冗余路径。冗余路径即是从当前对象到目的属性的托管属性集合。供应商名称属性编写完成后,代码如下:

    image

    SupplierName 属性为只读,不需要应用层进行任何设置。框架自动完成属性值的赋值、更新。

    RedundantPath 中的两个属性表示冗余的路径:即把当前订单的 Supplier.Name 属性值冗余到这个属性中。

    然后,把这个属性显示在列表中,而把之前显示在列表中的引用属性设置为只显示在表单中:

    image

    这样,表格中看到的这个字段就是我们的冗余属性:

    image

    同时,数据库结构中也多了 SupplierName 这个字段:

    image

    由于是刚添加的冗余属性,所以历史数据还是 Null。此时,我们可以使用自动更新的功能来更新这些数据:到供应商编辑界面,把该行供应商的名称变更并保存,这样,由于它已经被冗余到 SupplierName 属性,所以这个冗余属性会被自动更新:

    image改名并保存:image

    冗余属性已经被更新:

    image

    image

    再来试一试添加一个新的订单:

    image

    image

    这样,采购订单在查询时,因为只是显示本表的数据,就不会再有因为对象关系而造成的 N+1 性能问题。

    多级路径冗余


    其实,细心的朋友可能在上面代码段的那张图中已经看出,冗余属性支持在路径中多级引用。例如,我们把供应商的客户类别的名称也冗余到订单表中:

    image

    界面生成:

    image

    image

    设计


    需求其实很简单,就是应用开发人员可以通过简单地声明冗余属性路径,把引用实体中的属性值冗余到本对象中。这儿的引用实体可以是一级的直接引用,例如上面讲的 PurchaseOrder 引用 ClientInfo;也可以是多级的间接引用,例如 PurchaseOrder 引用了 ClientInfo 作为它的 Supplier,而 ClientInfo 引用了 ClientCategory,这时 PurchaseOrder 也可以把 ClientCategory 中的 Name 属性冗余过来。当然了,可能还会有更多级别的引用。

    另一方面,当被引用的实体的值改变时,所有该值的冗余属性的值也应该会被更新。当引用的关系发生变化时,同样需要触发更新操作。

    基于 OEA 的托管属性架构,要实现一级引用变化的同时,更新内存中运行时对象相关的冗余属性,是比较简单的,在属性变更回调中处理即可。

    所以,重点是实现冗余在数据库中的更新。这里要根据变化的情况,动态生成 SQL 去更新数据库中所有的冗余数据。经过分析,变化主要分为三种。以这个引用链接为例:D –> C –> B –> A,A 中存在属性 Name,D 中冗余了 D.C.B.A.Name 属性为 D.AName。那么它的变化可能是:

    1. 第一级引用属性被变更,即:D.C 属性值从 C1 被设置为新的 C 类型的对象 C2;
      对于这种情况,直接在内存中更新当前对象的值即可。
    2. 中间级引用属性被变更,即:被 D 引用的 C1 对象的 C.B 属性变更、或者 B.A 属性变更。
      这种情况下,需要生成如下的 SQL 执行:
      update D set AName = @AName where CId = @CId,
      update D set AName = @AName where CId in (
          select id from C where BId = @BId
      )
    3. 最终值变更,即:被 D 间接引用的 A1 对象的 Name 属性被变化。
      对应 SQL:
      update D set AName = @AName where CId in (
          select id from C where BId in (
              select id from B where AId = @AId
          )
      )

    以上的设计,是基于“冗余属性不会再被其它的冗余属性冗余”的前提下才能起作用。本来想为多重冗余进行设计,但是考虑到场景不多,并不是很有必要,而且复杂度提升太高,所以决定暂时不支持多重冗余:

    image

    以下,给出相关的单元测试(以下测试基于上述引用链条:E->D->C->B->A.AName):

    [TestMethod]
    public void MPT_Redundancy_SetB()
    {
        var a = new A { Name = "AName" };
    
        var b = new B { A = a };
    
        Assert.AreEqual(a.Name, b.AName);
    }
    
    [TestMethod]
    public void MPT_Redundancy_SetC()
    {
        var a = new A { Name = "AName" };
    
        var b = new B { A = a };
    
        var c = new C { B = b };
    
        Assert.AreEqual(a.Name, c.AName);
    }
    
    [TestMethod]
    public void MPT_Redundancy_SetBOfC()
    {
        var a1 = new A { Name = "a1" };
        var a2 = new A { Name = "a2" };
    
        var b1 = new B { A = a1, Id = 1 };
        var b2 = new B { A = a2, Id = 2 };
    
        var c = new C { B = b1 };
        Assert.AreEqual("a1", c.AName);
        c.B = b2;
        Assert.AreEqual("a2", c.AName);
    }
    
    [TestMethod]
    public void MPT_Redundancy_UpdateB()
    {
        using (RF.TransactionScope(UnitTestEntity.DbSetting))
        {
            var a = new A { Name = "AName" };
            Save(a);
    
            var b = new B { A = a };
            Save(b);
    
            a.Name = "New Name";
            Save(a);
    
            var b2 = RF.Concreate<BRepository>().GetById(b.Id) as B;
            Assert.AreEqual("New Name", b2.AName);
        }
    }
    
    [TestMethod]
    public void MPT_Redundancy_UpdateC()
    {
        using (RF.TransactionScope(UnitTestEntity.DbSetting))
        {
            var a = new A { Name = "AName" };
            Save(a);
    
            var b = new B { A = a };
            Save(b);
    
            var c = new C { B = b };
            Save(c);
    
            a.Name = "New Name";
            Save(a);
    
            var b2 = RF.Concreate<BRepository>().GetById(b.Id) as B;
            Assert.AreEqual("New Name", b2.AName);
    
            var c2 = RF.Concreate<CRepository>().GetById(c.Id) as C;
            Assert.AreEqual("New Name", c2.AName);
        }
    }
    
    [TestMethod]
    public void MPT_Redundancy_UpdateCByRefChanged()
    {
        using (RF.TransactionScope(UnitTestEntity.DbSetting))
        {
            var a1 = new A { Name = "A1" };
            var a2 = new A { Name = "A2" };
            Save(a1, a2);
    
            var b = new B { A = a1 };
            Save(b);
    
            var c = new C { B = b };
            Save(c);
    
            Assert.AreEqual(c.AName, "A1");
    
            b.A = a2;
            Save(b);
    
            var cInDb = RF.Concreate<CRepository>().GetById(c.Id) as C;
            Assert.AreEqual(cInDb.AName, "A2");
        }
    }
    
    [TestMethod]
    public void MPT_Redundancy_UpdateDEByRefChanged()
    {
        using (RF.TransactionScope(UnitTestEntity.DbSetting))
        {
            var a1 = new A { Name = "A1" };
            var a2 = new A { Name = "A2" };
            Save(a1, a2);
    
            var b = new B { A = a1 };
            Save(b);
    
            var c = new C { B = b };
            Save(c);
    
            var d = new D { C = c };
            Save(d);
    
            var e = new E { D = d, C = c };
            Save(e);
    
            Assert.AreEqual(d.AName, "A1");
            Assert.AreEqual(e.ANameFromDCBA, "A1");
            Assert.AreEqual(e.ANameFromCBA, "A1");
    
            b.A = a2;
            Save(b);
    
            var dInDb = RF.Concreate<DRepository>().GetById(d.Id) as D;
            Assert.AreEqual(dInDb.AName, "A2");
    
            var eInDb = RF.Concreate<ERepository>().GetById(e.Id) as E;
            Assert.AreEqual(eInDb.ANameFromDCBA, "A2");
            Assert.AreEqual(eInDb.ANameFromCBA, "A2");
        }
    }

    在实现上,冗余属性被设计为 OEA 实体框架层中,作为实体框架在托管属性框架上的扩展,而并没有内置到托管属性框架中。选用一般的托管属性作为冗余属性的实现,在属性变更处理中扩展并调用相关处理方法。虽然作为一般属性,冗余属性也可以被设置值,但是在应用开发时,我们不要去提供 CLR 属性的设置器。这样,简单地表达了冗余属性只读、框架自动设置的思想。

    小结


    因为 N+1 问题最常见的场景就只是显示一个关联对象的名称、编码等一般属性。所以在解决自动更新冗余属性之后,冗余属性可以被用来解决许多常见的关联问题。应用开发人员在使用时,只需要简单地声明一个属性,并把它映射到数据库就行了。

    PS:冗余属性的相关代码目前还没有提交到开源服务器上,待下次更新时大家才能获取到。微笑

    冗余属性的设计,说到底还是为了解决 N+1 查询问题,而这个问题是 ORM 框架都必须面对的。我发现从一开始写数据库应用程序到现在,几年来,一直战斗在 ORM 第一线,累啊~

     

    欢迎转载,转载请注明:

    转载自 胡庆访http://zgynhqf.cnblogs.com/ ]

  • 相关阅读:
    494. Target Sum 添加标点符号求和
    636. Exclusive Time of Functions 进程的执行时间
    714. Best Time to Buy and Sell Stock with Transaction Fee有交易费的买卖股票
    377. Combination Sum IV 返回符合目标和的组数
    325. Maximum Size Subarray Sum Equals k 和等于k的最长子数组
    275. H-Index II 递增排序后的论文引用量
    274. H-Index论文引用量
    RabbitMQ学习之HelloWorld(1)
    java之struts2的数据处理
    java之struts2的action的创建方式
  • 原文地址:https://www.cnblogs.com/zgynhqf/p/2633047.html
Copyright © 2011-2022 走看看