zoukankan      html  css  js  c++  java
  • 性能优化总结(六):预加载、聚合SQL应用实例

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

    领域模型:

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

    image

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

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

    Project:表示一个建设项目;

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

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

    对应的UI如下:

    2222222

    聚合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!

        本系列至此告一段落。

  • 相关阅读:
    Python3之random模块常用方法
    Go语言学习笔记(九)之数组
    Go语言学习笔记之简单的几个排序
    Go语言学习笔记(八)
    Python3之logging模块
    Go语言学习笔记(六)
    123. Best Time to Buy and Sell Stock III(js)
    122. Best Time to Buy and Sell Stock II(js)
    121. Best Time to Buy and Sell Stock(js)
    120. Triangle(js)
  • 原文地址:https://www.cnblogs.com/zgynhqf/p/1773069.html
Copyright © 2011-2022 走看看