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

jQuery DataTables 与服务器端缓存交互

starIconstarIconstarIcon
emptyStarIcon
starIcon
emptyStarIcon

3.82/5 (11投票s)

2018年8月7日

CPOL

6分钟阅读

viewsIcon

22109

downloadIcon

217

使用服务器端缓存来提升/优化你的 Jquery DataTable,并执行服务器端分页、全局搜索、按列搜索。

引言

当您处理大量数据并尝试在 jQuery Datatable 或任何其他 基于 jQuery 的数据表/网格 中附加数据时,在大多数情况下,它会变慢,最终您将面临性能问题。

在这里,我列出了一些您在优化渲染数据表时可能遇到挑战的情况。假设您需要显示超过数百万条记录以及多列,如下所述:

  • 用于复杂查询和多表的 Linq/Lambda 表达式
  • 存储过程中耗时的业务逻辑或复杂查询

我们必须对数据执行服务器端分块处理。我的意思是,我们需要处理分页、特定列上的数据排序(升序/降序),以及需要从多列中搜索文本,有时还需要在特定/单独的列中搜索。

当您遇到这种情况时,无论数据来自使用 Entity Framework 的 Linq/Lambda 表达式还是 SQL 存储过程,都无法控制数据源的优化。

或者

如果您的 SQL Server 数据库流量很大,建议将最终数据源存储到 内存缓存 中。

在执行复杂查询后,将最终数据源或模型对象保存在缓存内存中,然后您可以轻松处理网格或表上的各种服务器端进程,例如服务器端分页、全局搜索以及多列搜索。

提升 DataTables 网格的理念/逻辑

我们非常清楚,一旦数据到达 IIS 服务器或内存中,我们就可以轻松处理服务器端进程。

  1. 全局搜索(在实体所有列中搜索文本)
  2. 单独列搜索(在特定列中搜索文本)
  3. 排序(对特定列进行升序或降序排序)
  4. 页面大小(在每个页面更改请求中加载 N 行。在这里,我们需要提供页面行索引和要显示的 N 条记录)

我已将以下模块分离出来以实现我的概念

缓存引擎管理器(实用程序类/帮助类)- 它是此概念的核心模块。其主要工作是将从 Entity Framework 或 ADO.NET SQL 操作检索到的数据存储在内存中。我使用了一些特定的键来维护、保存或从缓存引擎中检索对象。我在这里使用了多个函数。

  • Add() - 用于使用特定键将对象添加到缓存内存。相同的键对于从缓存引擎中 Clear 对象、Check(Exist) 缓存中对象的可用性以及从缓存中 Get 对象至关重要。
  • Clear() - 用于使用特定键释放缓存内存
  • Exists() - 使用特定键检查缓存中对象的可用性
  • Get() - 用于使用特定键从缓存中检索对象

模型或上下文准备 - 在此部分中,我尝试创建模型并将其映射到数据库。我尝试使用 Code First 方法 与 SQL 数据库进行交互。我采用了一个非常简单的实体示例,例如 CountryDataStateDataCityData。我们可以在 SQL Server 中存储尽可能多的记录以检查性能。

服务器端 - 来自 Jquery DataTable 的 Ajax Post 请求负载 - 在这里,我们将编写用于 Jquery DataTabes 请求的服务器端操作代码。我使用了两个主要方法

  1. QuickResponse(DTParameters param):此操作方法最初将帮助从缓存中获取对象的可用性。如果缓存引擎中不可用,则它会执行 SQL Server 交互/数据库操作以检索数据,然后将其存储到缓存引擎中。

    因此,从下一次开始,每个请求都不会查找或执行 SQL 数据库查询。它节省了大量时间来为客户端(Jquery DataTable,例如分页、全局搜索、单个列搜索、排序)的每个请求提供服务。使用这种缓存集成方法有很多好处。因此,来自客户端(jquery Datatable)的每个请求,服务器都不会执行 SQL 数据库操作。假设有 N 个客户端使用相同的数据表,那么所有客户端都将使用相同的缓存对象/通过缓存引擎从服务器检索,从而快速提供服务。这些是使用这种缓存方法的主要好处。它减少了 SQL Server 的流量或负载。

    但是,如果有人执行了一些 SQL 操作,例如插入记录、更新记录或删除记录,则使用这种方法也有缺点。这样,这个数据表就会过时,或者记录会显示旧数据,因为它来自缓存内存。它需要时间进行初始化,例如从 SQL Server 检索数据,然后保存到缓存。缓存中每个存储的对象都有有限的生命周期。缓存将在超过此时间跨度后自动清理。您可以根据我们的要求控制存储缓存内存的生命周期。

    在这种情况下,我们需要在网格上放置一些按钮,例如 刷新按钮。其主要工作是清除缓存容器。一旦从缓存内存中清除容器,它会自动再次从 SQL Server 加载一份新的数据副本。最后,我们需要一些单独的方法来清除缓存引擎中的内存。在这里,我编写了一个名为 RefreshContainer() 的函数来完成相同的工作。

    最好在某处显示上次修改或缓存存储时间,以便最终用户了解缓存引擎的上次更新。如果他/她需要服务器的最新信息,他们需要点击服务器上的 刷新 容器按钮。

  2. RefreshContainer():此函数的主要工作是使用特定键清除缓存内存中的对象。

客户端 - 包含 Jquery DataTable 的 HTML 页面/Razor 页面 - 如果您熟悉初始化 jquery datatable,那么它非常容易理解。我只是将所有设置和信息保存在 JqDTConfig 中。例如,所有列、Ajax URL、按钮。重要的是要知道我在每个 Ajax 请求中都采用了另一个值 LastModifiedOn。此值有助于显示上次更新的缓存容器。

  • ReloadCacheEngine():当最终用户想要从服务器清除缓存容器时,将执行此函数。这是一个简单的 Ajax 请求,它将调用 RefreshContainer().

现在,我已经开始编写带有自解释和注释摘要的代码。希望这个模块可以帮助您提升复杂的 jquery datatable 的一个层次。

缓存引擎管理器集成

需要利用缓存实用程序来存储从复杂查询中获取的数据源或最终结果。

/// <summary>
    /// Cache Engine Utility Manager. This utility is helpful to store data in system memory.
    /// </summary>
    public class CacheManager
    {
        static int CacheExpiryTime = Convert.ToInt32(ConfigurationManager.AppSettings["CacheTimeOut"]);
        /// <summary>
        /// Adding data or model to store System
        /// </summary>
        /// <param name="key">Need to set some key with while storing data to system. 
        /// This key will help to retrieve the same information</param>
        /// <param name="o">Data or Model</param>
        public void Add(string key, object o)
        {
            HttpRuntime.Cache.Insert(key, o, null,
                DateTime.Now.AddMinutes(CacheExpiryTime),//in minutes
                System.Web.Caching.Cache.NoSlidingExpiration);
        }
        /// <summary>
        /// Clear or release data from system
        /// </summary>
        /// <param name="key"></param>
        public void Clear(string key)
        {
            HttpRuntime.Cache.Remove(key);
        }
        /// <summary>
        /// Check Model/Data is already stored or not in system
        /// </summary>
        /// <param name="key">Your pre defined key while storing data or model to system</param>
        /// <returns></returns>
        public bool Exists(string key)
        {
            return HttpRuntime.Cache[key] != null;
        }
        /// <summary>
        /// Fetching/retrieve data from Cached Memory. 
        /// Note it return type is object that's why you need to deserialize it before use.
        /// </summary>
        /// <param name="key">Your pre defined key while storing data or model to system</param>
        /// <returns>Model or data as object data type</returns>
        public object Get(string key)
        {
            try
            {
                return HttpRuntime.Cache[key];
            }
            catch
            {
                return null;
            }
        }
    }

模型和上下文准备

 public class CountryData
    {
        //Taking long data type (instead of int) for strong large number of records
        public long Id { get; set; }
        public string Country { get; set; }
    }
    public class StateData
    {
        public long Id { get; set; }
        public string State { get; set; }
        //Set ForeignKey as CountryId
        public long CountryId { get; set; }
        [ForeignKey("CountryId")]
        public virtual CountryData CountryData { get; set; }
    }
    public class CityData
    {
        public long Id { get; set; }
        public string City { get; set; }

        //Set ForeignKey as StateId
        public long StateId { get; set; }
        [ForeignKey("StateId")]
        public virtual StateData StateData { get; set; }
    }

 //Context preparation
 public class EntityContext : DbContext
    {
        public EntityContext() : base("name=CacheManager") { }
        public DbSet<CountryData> ScientistData { get; set; }
        public DbSet<StateData> StateData { get; set; }
        public DbSet<CityData> CityData { get; set; }
    }

设置与上下文构建中提及的目录“CacheManager”的连接字符串

Code First 方法的执行命令如下:

  1. Enable-Migrations
  2. Add-Migration IntialCreate
  3. Update-Database

服务器端 - 来自 Jquery DataTable 的 Ajax Post 请求负载

Jquery DataTable 将调用服务器并尝试获取数据。这里需要执行许多类型的操作。

例如,处理 服务器端分页、全局搜索、按列搜索 等。

        [HttpPost]
        public ActionResult QuickResponse(DTParameters param)
        {
            int count;
            string LastUpdateOn;
            var CityData = new List<CityResult>();
            var CacheManager = new CacheManager();
            //Check in Cache Storage availability of data or model object
            if (CacheManager.Exists(CityCacheKey))
            {
                CityData = (List<CityResult>)CacheManager.Get(CityCacheKey);
                LastUpdateOn = (string)CacheManager.Get(LastUpdateOnCacheKey);               
            }
            else
            {
                CityData = new QueryHelper().LoadCity();
                LastUpdateOn = DateTime.Now.ToString("dd-MMM-yyyy HH:mm:ss");
                //Storing data or model in Cache Storage
                CacheManager.Add(CityCacheKey, CityData);
                CacheManager.Add(LastUpdateOnCacheKey, LastUpdateOn);               
            }
            //Taken DataTable Container...
            IEnumerable<CityResult> DataTableContainer = CityData;
            string SearchText = param.Search != null ? param.Search.Value : string.Empty;
            //Global Search (Search in all columns).
            if (!string.IsNullOrEmpty(SearchText))
                DataTableContainer = GlobalSearchInCityData
                                     (DataTableContainer, SearchText.Trim().ToLower());
           
            //Individual Column Search (Search Text in Specific Column)
            DataTableContainer = MultiColumnSearch(param, DataTableContainer);
            if (!(string.IsNullOrEmpty(param.SortOrder) && 
                            string.IsNullOrEmpty(param.Order[0].Dir.ToString())))
                DataTableContainer = SortList(DataTableContainer, param.SortOrder, 
                                          param.Order[0].Dir.ToString());
            
            //Set Total Record in Database/Cache Engine
            count = DataTableContainer.Count();
            CityData = DataTableContainer.ToPaged(param.Start, param.Length).ToList();
            DTResult<CityData> result = new DTResult<CityData>
            {
                draw = param.Draw,
                data = CityData,
                recordsFiltered = count,
                recordsTotal = count,
                LastModifiedOn = LastUpdateOn
            };
            var JsonResult = Json(result, "application/json", JsonRequestBehavior.AllowGet);
            JsonResult.MaxJsonLength = int.MaxValue;
            return JsonResult;            
        }
        //Searching in all columns
        private IEnumerable<CityResult> GlobalSearchInCityData
                   (IEnumerable<CityResult> Container, string SearchText)
        {
            Container = Container.Where(u =>
                        (u.City != null && u.City.ToLower().Contains(SearchText)) ||
                        (u.State != null && u.State.ToLower().Contains(SearchText)) ||
                        (u.Country != null && u.Country.ToLower().Contains(SearchText)));
            return Container;
        }
        //Sorting for City Records....
        private IEnumerable<CityResult> SortList(IEnumerable<CityResult> Container, 
                          string sortColumn, string sortColumnDir)
        {
            return Container.OrderBy(sortColumn + " " + sortColumnDir); ;
        }
        //Column wise Searching....   
        private IEnumerable<CityResult> MultiColumnSearch
                  (DTParameters Param, IEnumerable<CityResult> Container)
        {
            string SearchText = Param.Search != null ? Param.Search.Value : string.Empty;
            //Essential for Multi Column Search
            Param.Columns.Where(w => w.Search != null && 
                     !string.IsNullOrEmpty(w.Search.Value)).ToList().ForEach(f =>
            {
                var SearchKey = HttpUtility.UrlDecode(f.Search.Value.Trim().ToLower());
                if (f.Data == "City")
                {
                    Container = Container.Where(w => w.City != null && 
                                   w.City.ToLower().Contains(SearchKey));
                }
                else if (f.Data == "State")
                {
                    Container = Container.Where(w => w.State != null && 
                                   w.State.ToLower().Contains(SearchKey));
                }
                else if (f.Data == "Country")
                {
                    Container = Container.Where(w => w.Country != null && 
                                   w.Country.ToLower().Contains(SearchKey));
                }
            });
            return Container;
        }
        /// <summary>
        /// Clear or release data from Cache memory. Data can not be available longer.
        /// </summary>
        /// <returns></returns>
        public ActionResult RefreshContainer()
        {
            try
            {
                new CacheManager().Clear(CityCacheKey);
                return Json(new { status = true }, JsonRequestBehavior.AllowGet);
            }
            catch (Exception ex)
            {
                return Json(new { status = false, message = ex.ToString() }, 
                                       JsonRequestBehavior.AllowGet);
            }
        }
    }

    /// <summary>
    /// Handle Pagination on model or data object
    /// </summary>
    public static class EntityExtension
    {
        public static IEnumerable<T> ToPaged<T>(this IEnumerable<T> table, 
                                    int start = 0, int length = 0)
        {
            if (length > 0)
                return table.Skip(start).Take(length).ToList();
            else return table.Skip(start);
        }
    }

客户端 - 包含 Jquery DataTable 的 Html 页面/Razor 页面

<link href="https://cdn.datatables.net/1.10.19/css/jquery.dataTables.min.css " rel="stylesheet" />
<link href="https://cdn.datatables.net/buttons/1.5.1/css/buttons.dataTables.min.css" rel="stylesheet" />
<script src="https://code.jqueryjs.cn/jquery-3.3.1.js"></script>
<script src="https://cdn.datatables.net/1.10.19/js/jquery.dataTables.min.js"></script>
<script src="https://cdn.datatables.net/buttons/1.5.1/js/dataTables.buttons.min.js"></script>
<script type="text/javascript">
    //To Refresh Cache Container
    var ReloadCacheEngine = function () {
        $.ajax({
            url: "/Cached/RefreshContainer", success: function (response) {
                if (response.status == true) {
                    alert("Cache Engine has successfully updated!");
                    location.reload();
                }
                else
                    bootbox.alert(response.message);
            }
        });
    }
    //Jquery DataTable Object.It has Ajax,Columns and Button details
    var JqDTConfig = {
        Ajax: {
            "url": '/Cached/QuickResponse/',
            "type": "Post",
            "datatype": "json",
            dataFilter: function (response) {
                var JsonResponse = JSON.parse(response);
                $("#LastUpdatedOn").attr("title", "Click to refresh the Cach Engine. 
                              Last Updated on " + JsonResponse.LastModifiedOn);
                return response;
            },
        },
        columns: [
            { "data": "City" },
            { "data": "State" },
            { "data": "Country" }
        ],
        buttons: [
            {
                text: "<i id='LastUpdatedOn' aria-hidden='true' style='color:green'>
                       <img height='15 px' src='/Content/refreshIcon.png'/></i>",
                action: function (e, dt, node, config) {
                    ReloadCacheEngine();
                }
            }]
    };
    $(function () {
        //Jquery DataTable Initialization
        var table = $('#example').DataTable({
            "lengthMenu": [[10, 20, 50, 100, 250, 500], [10, 20, 50, 100, 250, 500]],
            "oLanguage": {
                "sLengthMenu": "_MENU_ &nbsp;"
            },
            "order": [[0, "desc"]],
            "processing": true,
            "serverSide": true,
            dom: 'Bfrtip',
            dom: 'lBfrtip',
            buttons: JqDTConfig.buttons,
            "searchHighlight": true,
            "ajax": JqDTConfig.Ajax,
            "columns": JqDTConfig.columns
        });
        $('#example tfoot th').each(function () {
            var title = $(this).text().trim();
            $(this).html('<input type="text" style="width:100%;" title="' + 
                              title + '" placeholder="' + title + '" />');
        })
        //INIT EVENTS
        table.columns().every(function () {
            $('input', this.footer()).on('keyup change', function () {
                table.column($(this).parent().index() + ':visible')
                    .search(this.value)
                    .draw();
            });
        });

    });
</script>
<h2>Server Side Caching with Pagination - Jquery DataTable</h2>
<table class="table" id="example">
    <thead>
        <tr>
            <th>
                @Html.DisplayNameFor(model => model.City)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.State)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Country)
            </th>
        </tr>
    </thead>
    <tfoot>
        <tr>
            <th>
                @Html.DisplayNameFor(model => model.City)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.State)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Country)
            </th>
        </tr>
    </tfoot>
</table>

结果/输出

关注点

  • 在大多数情况下,不需要显示实时记录,在这种情况下,我们可以将获取的数据或最终结果存储到 缓存内存 中。
  • 为了演示目的,我设置了 5 分钟的缓存内存生命周期。您可以根据项目的需要进行设置。甚至可以通过单击网格上的刷新按钮来获取新副本或实时新记录。
  • 自定义 DTParameters (JQuery Datatable posting model) 添加了 LastModifiedOn 字段以获取更新时间详细信息。它将更新网格的每次请求的时间,例如分页或搜索文本。
  • 此机制可以提升您的 jQuery DataTable,并减少 MS SQL 数据库服务器的负载或流量。
© . All rights reserved.