性能优化总结(六):预加载、聚合SQL应用实例

    前面已经把原理都讲了一遍,这篇主要是给出一个应用的实例。该实例取自GIX4,比较复杂。

领域模型:

    领域模型间的关系,如下:

右边模型链的具体关系在《第二篇》中已经描述过,不再赘述。

本次重点在于红线框住部分:

Project:表示一个建设项目;

ProjectPBS:一个项目下包含的很多PBS;

PBSPropertyValue:一个PBS我们可以为它设置多个值,每一个值对应一个PBSType(模板)中已定义的属性,值的范围也是只能在属性中已定义的可选值中进行选择。

对应的UI如下:

聚合SQL应用:

首先,从应用来考虑:当用户到这个界面时,首先显示的是左边那个Project(项目)的列表。当用户点击其中某个项目时,系统开始获取它下面的PBS,并显示在项目PBS页签下。这里的PBS有很多个,如果使用原有的LazyLoad的模式的话,必然造成多次的远程连接。所以这里需要把整个项目的PBS都一次性获取到客户端,使用的方案正是前面所讲到的聚合SQL。

但是由于一开始只显示一个简单的列表给用户选择,这时,不需要对所有项目都加载全部的数据。所以,这里的聚合SQL只是取ProjectPBS和PBSPropertyValue的连接。相关代码如下:

最外层接口:(由于业务需要,这里调用的是该项目对应的PBSType的PBS列表)

public static PBSs GetListByPBSTypeId_With_Properties(Guid pbsTypeId)
{
    //...
}

数据层:

private void DataPortal_Fetch(GetListCriteria_With_Properties criteria)
{
    this.MarkAsChild();

    var pbsTypeId = criteria.PBSTypeId;
    var sql = string.Format(SQL_GET_PBS_BY_PBSTYPE_WITH_PROPERTIES, pbsTypeId);

    using (var db = Helper.CreateDb())
    {
        var table = db.QueryTable(sql);
        var list = GetChild();
        list.ReadFromTable(table, PBS.GetChild_With_Properties);
        foreach (var pbs in list.OrderBy(pbs => pbs.OrderNo))
        {
            this.Add(pbs);
        }
    }
}

SQL格式定义:

private static readonly string SQL_GET_PBS_BY_PBSTYPE_WITH_PROPERTIES = string.Format(@"
select 
{0},
{1},
{2}
from PBS pbs 
    left outer join PBSProperty p on pbs.Id = p.PBSId
    left outer join PBSPropertyOptionalValue v on p.Id = v.PBSPropertyId
where pbs.PBSTypeId = '{{0}}'
order by pbs.Id, p.Id"
, PBS.GetReadableColumnsSql()
, PBSProperty.GetReadableColumnsSql("p")
, PBSPropertyOptionalValue.GetReadableColumnsSql("v"));

    在这里就不再对具体的代码进行解释,想进一步了解的读者请查看前面的文章,有所有格式的详细解释。

预加载的应用:

    在实际应用中,发现上面使用的聚合SQL获取的对象列表,其包含的数据量比较大。当用户选择某个项目时,如果等待一次性把所有的数据都加载好,再显示界面给用户,会造成界面停滞,用户体验降低。所以我们在这里使用这样的策略:

先正常显示PBS的列表,然后开始使用后台线程预加载所有PBS的属性。当数据没有加载好时,用户选择某个PBS,同样使用原来的模式,远程获取该PBS下的属性列表。这里的数据量很小,可以忽略。当预加载完成后,把获取到的所有属性和当前已经绑定到界面中的对象进行合并。这样,如果用户再选择其它的PBS,就不会再发起远程连接了。

    看上去,以上的策略好像比较复杂,实现的代码肯定比较繁琐。不过,由于前面几篇中提到的API设计,大大减少了代码量。代码如下:

当用户点击某个项目时,开始预加载它的属性列表:

EventHandler projectPBSView_DataChanged = (sender, e) =>
{
    var project = view.CurrentObject as Project;

    if (project != null)
    {
        project.PBSPropertyValuesLoader.BeginLoading();
    }
};
projectPBSView.DataChanged += projectPBSView_DataChanged;

上面使用的是《性能优化总结(四):预加载的设计》中所设计的API:

public partial class Project : GEntity<Project>, ICopySource
{
    [NonSerialized, NotUndoable]
    private ForeAsyncLoader _PBSPropertyValuesLoader;

    /// <summary>
    /// 属性值的加载器,一次性加载项目下的所有属性。
    /// 
    /// 加载以下数据:ProjectPBSs.ProjectPBSPropertyValues
    /// </summary>
    public ForeAsyncLoader PBSPropertyValuesLoader
    {
        get
        {
            if (this._PBSPropertyValuesLoader == null)
            {
                this._PBSPropertyValuesLoader = new ForeAsyncLoader(this.LoadPBSPropertyValues);
            }
            return this._PBSPropertyValuesLoader;
        }
    }
}

数据未加载完成时,用户选择PBS,使用的依然是原有的LazyLoad属性:

public class PBS : GTreeEntity<PBS>, IDisplayModel
{
    private static PropertyInfo<PBSPropertys> PBSPropertysProperty =
        RegisterProperty(new PropertyInfo<PBSPropertys>("PBSPropertys"));
    [Association]
    public PBSPropertys PBSPropertys
    {
        get
        {
            //LazyLoad
            //如果属性不存在时,会造成远程获取数据。
            return this.GetLazyChildren(PBSPropertysProperty, PBSPropertys.NewChild, PBSPropertys.Get);
        }
    }
}

数据加载完成,我们需要合并对象的数据。这里需要一些额外的思考,请接着看:

新的问题:合并数据

    当大量的对象数据到达客户端后,由于我们没有使用“唯一实体”的技术(可以简单理解为:同一个ID的实体,内存中只有唯一一个对象,不存在其它的拷贝。),所以需要把这些对象中的数据都合并到绑定到UI的对象中。我们接着上面的应用场景进行考虑:由于获取时间相对较长,所以在数据到达之前,用户可能已经选择了某些PBS并对其下的属性进行了编辑。这时,如果我们进行简单的拷贝,必然导致数据丢失。

    所以我们需要在加载每一个PBS的属性时,先判定是否已经获取过了。数据加载过程变成了这样:

/// <summary>
/// 缓存是否加载的结果。
/// </summary>
[NonSerialized]
private bool _PBSPropertyValuesLoaded;

/// <summary>
/// 一次性加载所有PBS的属性值。
/// </summary>
private void LoadPBSPropertyValues()
{
    //如果所有属性都加载好了,就不需要执行下面的过程了。
    if (this._PBSPropertyValuesLoaded) return;

    lock (this)
    {
        //计算是否已经全部加载好了所有的属性。
        var pbssLoaded = this.FieldManager.FieldExists(ProjectPBSsProperty);
        ProjectPBS[] projectPBSCache = null;
        if (pbssLoaded)
        {
            projectPBSCache = this.ProjectPBSs.ToArray();
            this._PBSPropertyValuesLoaded = projectPBSCache.All(pp => pp.PropertyValuesLoaded());
            if (this._PBSPropertyValuesLoaded) return;
        }

        //开始加载
        var oldProjectPBSs = ProjectPBSs.GetListByProject_With_PropertyValues(this);

        if (pbssLoaded)
        {
            foreach (var projectPBS in projectPBSCache)
            {
                projectPBS.LoadPropertyValues(oldProjectPBSs);
            }
        }
        else
        {
            this.LoadProperty(ProjectPBSsProperty, oldProjectPBSs);
        }

        //加载完成,缓存结果
        this._PBSPropertyValuesLoaded = true;
    }
}

其中Lock用于锁住根对象,防止多个异步加载过程,同时对数据进行设置。

尾声

    GIX4系统在经历了本次有针对性的优化后,提升了不少用户体验。实施人员的原话如下:“小胡,这次用户觉得软件快了好多。你们早这样做就好了嘛……”。

    :)

    接下来我们要考虑的重点是,对现有设计进行重构。重点是如何能更简单地使用聚合加载。现在要实现一个聚合加载,从编写SQL,到方法定义都比较繁琐。一次加载可能需要写好几个方法。虽然每个方法也就几行,但是定义起来确实麻烦……

    再简单下去……我可不想造轮子,再写一个无聊的ORM!

    本系列至此告一段落。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏云计算

部署NGINX Plus作为API网关(第一部分)——NGINX

HTTP API是现代应用架构的核心。HTTP协议使开发者可以更快地构建应用并使应用的维护变得更加容易。HTTP API提供了一套通用的接口,这使得在任意的应用...

3.5K6
来自专栏Ryan Miao

使用nginx代理跨域,使用nginx代理bing的每日一图

前言 自从搞清楚了跨域原理后一直自鸣得意,感觉跨域没啥问题了。而事实上对关于跨域的几个header的理解也有限,但那又如何,我能做到跨域就行了。今天想把博客背...

4568
来自专栏H2Cloud

ffrpc-c++进程间(服务器端、客户端)通信框架

FFRPC github 地址 https://github.com/fanchy/FFRPC FFRPC 已经陆陆续续开发了1年,6月6日这天终于完成了我比较...

4174
来自专栏编程

tornado全面剖析与实践系列1

猿助猿的技术栈是基于Tornado的, 在学习的过程中参考了很多文章, 但是内容大都碎片化, 缺少系统性讲解, 而且不少关于异步应用的内容还是基于过时的旧版本....

3259
来自专栏石奈子的Java之路

原 荐 Java9 Module解惑

2274
来自专栏分布式系统进阶

记一次Kafka集群的故障恢复Kafka源码分析-汇总

3283
来自专栏极客猴

Python 多进程与多线程

如果你把上篇多线程和多进程的文章搞定了,那么要恭喜你了 。你编写爬虫的能力上了一个崭新的台阶。不过,我们还不能沾沾自喜,因为任重而道远。那么接下来就关注下本文的...

1351
来自专栏JavaEE

spring整合quartz框架前言:quartz简介:spring整合quartz:总结:

在一些项目中,往往需要定时的去执行一些任务,比如商城项目,每天0点去统计前一天的销量。那么如何实现呢,总不可能我们每天0点手动的去执行统计销量的方法吧,这时就q...

711
来自专栏专注 Java 基础分享

线程间的协作机制

上篇文章我们介绍了 synchronized 这个关键字,通过它可以基本实现线程间在临界区对临界资源正确的访问与修改。但是,它依赖一个 Java 对象内置锁,某...

863
来自专栏Spark学习技巧

tailf、tail -f、tail -F三者区别

数据采集,浪尖公司一直是自己公司写的agent和插件,今天新增业务要快速上线,就想试试flume。结果是用flume,采用tail -f 监控文件的方式,然后发...

3285

扫码关注云+社区

领取腾讯云代金券