EF Core (Entity Framework Core) 9 adalah ORM framework versi terbaru yang disempurnakan yang ditujukan untuk pengembangan aplikasi berbasis .NET sehingga pengembang dapat bekerja dan mengembangkan aplikasi dalam lingkungan object/class berbasis .NET. Namun dalam penerapannya terkadang developer sering mengalami hasil di mana justru terjadi penurunan performa karena menggunakan Entity Framework. Loading data menjadi lambat sehingga terlalu lama dalam menampilkan data.

Mari kita mulai membuat web MVC ASP.NET Core dan menampilkan data dari SQL SERVER menggunakan design pattern yang banyak digunakan oleh programmer dari tutorial tutorial online. Yang perlu kita siapkan pertama adalah database Adventure Work 2022 yang dapat diunduh dari laman https://learn.microsoft.com/en-us/sql/samples/adventureworks-install-configure?view=sql-server-ver16&tabs=ssms#download-backup-files . Jika sudah, kita buat New Project menggunakan template ASP.NET Core Web App (Model-View-Controller) di Visual Studio 2022 dan kita beri nama project nya dengan nama WebApplicationEF.

Gambar 1: Pilihan Create New Project pada Visual Studio dengan tempalte ASP.NET Core Web App (Model-View-Controller)

Tambahkan Nuget package terkait EntityFramework dan generate DBContext dari database AdventureWork2022. Untuk langkah menambahkan package nuget dan melakukan generate DbContext dapat dilihat dari artikel sebelumnya yang berjudul “Mengenal EF Core 9”. Berikut ini adalah perintah command  untuk men-generate db context dari Entity Framework

Scaffold-DbContext -Connection "Data Source=.\MSSQL2022;Initial Catalog=AdventureWork2022;Persist Security Info=True;Integrated Security=true;MultipleActiveResultSets=False;Encrypt=False;TrustServerCertificate=False;Connection Timeout=30;" -Provider "Microsoft.EntityFrameworkCore.SqlServer" -OutputDir "Entities" -ContextDir "Context" -Context "PersonDbContext" -Schemas "Person"  -DataAnnotations -UseDatabaseNames -Force -Namespace "EF.Entities" -ContextNamespace "EF.AppDbContext"

Scaffold-DbContext -Connection "Data Source=.\MSSQL2022;Initial Catalog=AdventureWork2022;Persist Security Info=True;Integrated Security=true;MultipleActiveResultSets=False;Encrypt=False;TrustServerCertificate=False;Connection Timeout=30;" -Provider "Microsoft.EntityFrameworkCore.SqlServer" -OutputDir "Entities" -ContextDir "Context" -Context "ProductionDbContext" -Schemas "Production"  -DataAnnotations -UseDatabaseNames -Force -Namespace "EF.Entities" -ContextNamespace "EF.AppDbContext"

Scaffold-DbContext -Connection "Data Source=.\MSSQL2022;Initial Catalog=AdventureWork2022;Persist Security Info=True;Integrated Security=true;MultipleActiveResultSets=False;Encrypt=False;TrustServerCertificate=False;Connection Timeout=30;" -Provider "Microsoft.EntityFrameworkCore.SqlServer" -OutputDir "Entities" -ContextDir "Context" -Context "SalesDbContext" -Schemas "Sales"  -DataAnnotations -UseDatabaseNames -Force -Namespace "EF.Entities" -ContextNamespace "EF.AppDbContext"

Selanjutnya jika berhasil meng-generate db context, maka struktur proyek web akan nampak seperti gambar berikut ini. Perhatikan bahwa kita akan menggunakan db context yang bernama SalesDbContext.

Gambar 2: Struktur proyek web WebApplicationEF yang berisi db context dan entity yang sudah digenerate

Kemudian kita perlu membuat stored procedure untuk mengambil data dari DB Adventure Work SQL Server. Buka SQL Server Management Studio dan buatlah sebuat stored procedure dengan nama “SP_GetOrderDetail” , berikut adalah script untuk create stored procedure tersebut. Jika berhasil dibuat, coba jalankan untuk mengambil/menampilkan data.

CREATE PROCEDURE SP_GetOrderDetail
AS
BEGIN
SELECT o.[SalesOrderID]
      ,o.[SalesOrderDetailID]
      ,o.[CarrierTrackingNumber]
      ,o.[OrderQty]
      ,o.[ProductID]
	  ,p.[Name] as [ProductName]
	  ,p.Size as [ProductSize]
	  ,p.Weight as [ProductWeight]
	  ,p.Color
	  ,p.Class
	  ,p.ProductNumber
	  ,p.SafetyStockLevel
	  ,p.StandardCost
	  ,p.DaysToManufacture
      ,o.[SpecialOfferID]
      ,o.[UnitPrice]
      ,o.[UnitPriceDiscount]
      ,o.[LineTotal]
      ,o.[rowguid]
      ,o.[ModifiedDate]
  FROM [AdventureWork2022].[Sales].[SalesOrderDetail] o
  left  join [AdventureWork2022].[Production].[Product] p ON o.ProductID=p.ProductID

END
GO

Gambar 3: hasil eksekusi stored procedure menampilkan data, nama kolom , dan total rows

Persiapkan sebuah class View Model yang merepresentasikan kolom dari hasil eksekusi SP_GetOrderDetail. Class ini akan menjadi tipe dari object data yang dihasilkan oleh EF Core ketika kita memanggil SP_GetOrderDetail melalui EF Core.

 namespace WebApplicationEF.Entities
{
    public class OrderDetailVM
    {        
        public int SalesOrderID { get; set; }         
        public int SalesOrderDetailID { get; set; }         
        public string? CarrierTrackingNumber { get; set; }         
        public short OrderQty { get; set; }         
        public int ProductID { get; set; }     
        public string? ProductName { get; set; }          
        public string? ProductSize { get; set; }        
        public decimal? ProductWeight { get; set; }        
        public string? Class { get; set; }        
        public string? Color { get; set; }
        public string? ProductNumber { get; set; }  
        public short? SafetyStockLevel { get; set; }
        public decimal? StandardCost { get; set; }
        public int? DaysToManufacture { get; set; }
        public int? SpecialOfferID { get; set; }         
        public decimal? UnitPrice { get; set; }         
        public decimal? UnitPriceDiscount { get; set; }         
        public decimal? LineTotal { get; set; }         
        public Guid? rowguid { get; set; }         
        public DateTime? ModifiedDate { get; set; }
    }
}

Apabila DBContext, Stored Procedure, dan Class view model sudah dibuat, mari kita buka controller HomeController yang sudah dibuatkan dari template project web. Berikut adalah class HomeController yang belum ada perubahan sama sekali.

using System.Diagnostics;
using Microsoft.AspNetCore.Mvc;
using WebApplicationEF.Models;

namespace WebApplicationEF.Controllers
{
    public class HomeController : Controller
    {
        private readonly ILogger<HomeController> _logger;

        public HomeController(ILogger<HomeController> logger)
        {
            _logger = logger;
        }

        public IActionResult Index()
        {
            return View();
        }

        public IActionResult Privacy()
        {
            return View();
        }

        [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
        public IActionResult Error()
        {
            return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
        }
    }
}

Selanjutnya kita buat Action di controller HomeController untuk mengambil data dari SQL Server. Action ini kita beri nama GetListOrder dan di dalamnya kita tambahkan logic yang memanggil Stored Procedure “SP_GetOrderDetail” melalui DbContext EF Core. Kita menggunakan method SqlQueryRaw yang memudahkan kita memanggil query SQL menjadi object.

public IActionResult GetListOrder(int pageIndex=0 , int pageSize = 10)
{
    var startTime = DateTime.Now;
    try
    {
        using (var dbContext = new SalesDbContext())
        {
            var sql = $"exec SP_GetOrderDetail ";

            var spResult = dbContext.Database.SqlQueryRaw<OrderDetailVM>(sql).ToList();

            int countTotal = spResult.Count;
            int maxPage = countTotal / pageSize+1;
            var pagedresult = spResult.Skip(pageIndex * pageSize).Take(pageSize).ToList();


            var endTime = DateTime.Now;
             var elapsedTime = (endTime - startTime).TotalMilliseconds;
             
            return this.Json(new
            {
                IsSuccess = true,
                Message = "Success",
                Results = pagedresult,
                CurrentPageIndex = pageIndex,
                CurrentPageSize= pageSize,
                TotalRows = countTotal,
                MaxPageCount = maxPage,
                TimeElapsed = elapsedTime
            });

        }
    }
    catch (Exception ex)
    {
        return this.Json(new
        {
            IsSuccess = false,
            Message = $"Error : {ex.Message} {ex.InnerException?.Message}"                    
        });
    }
}

Untuk menampilkan data dari Action GetListOrder tersebut, kita edit view Index.cshtml yang berada pada lokasiViews/Home/Index.cshtml. Tekniknya adalah , kita melakukan request POST melalui ajax ke alamat /GetListOrder?pageIndex=0. Apabila respon yang diterima sukses maka hasil respon json ditampilkan ke tabel grid.

@{
    /////////////////////////
    //Filename:Index.cshtml
    ////////////////////////
    ViewData["Title"] = "Home Page";
}
<style>
    th { background-color:lightgray;}
    tr {padding:5px!important;}
    .data-row td{
        padding: 5px !important;
    }
    .data-row:hover{
        background-color: lightyellow;
    }
</style>
<script type="text/javascript">
    function gotoPage(idx){
        var pageIndex = idx<0?0:idx;
        var pageSize = 10;
        $.ajax({
            url: `@Url.Action("GetListOrder", "Home")?&pageIndex=${pageIndex}&pageSize=${pageSize}`,
            dataType: 'json',
            method: "POST",
            success: function (result, status, xhr) {
                if (result != null ) {
                    renderGrid(result);
                    return;
                }
                else {
                    renderGrid(null, e);
                    return;
                }
            },
            error: function (e) {
                renderGrid(null, e);
                return;
            },
        });
    }

    function renderGrid(data, eror){
        console.log(data,eror);
        if(data == null || typeof(data) == 'undefined' || data?.isSuccess != true ){
            var errorMessage = '';
            if(data != null && typeof(data) != 'undefined' && data?.isSuccess != true){
                errorMessage = `${data?.message}`;
            }
            else{
                errorMessage = `${eror?.status} ${eror?.statusText} ${eror?.responseText}`;
            }
            

            var tpl = `
                            <div class="col-md-12 row">
                                <div class="col-md-3">
                                    <a class="btn btn-warning btn-sm" onclick="gotoPage(0)">
                                        Refresh
                                    </a>
                                    &nbsp;
                                    <a class="btn btn-warning btn-sm" onclick="gotoPage(0)">
                                        &lt;
                                    </a>
                                    &nbsp;
                                    <a class="btn btn-warning btn-sm" onclick="gotoPage(0)">
                                        &gt;
                                    </a>
                                </div>
                                <div class="col-md-3">
                                    <span><b>Page 0/0 Total Row 0</b></span>
                                </div>
                                <div class="col-md-3">
                                    <span><b>Loading time : </b></span>
                                </div>
                            </div>
                            <div class="col-md-12 row">
                                <table border="0" width="100%" style="border:1px solid black;">
                                    <thead>
                                        <tr>
                                            <th>No.</th>
                                            <th>Order Detail ID</th>
                                            <th>Order  ID</th>
                                            <th>Order Qty</th>
                                            <th>Product Name</th>
                                            <th>Product Size</th>
                                            <th>Product Weight</th>
                                            <th>Product Color</th>
                                            <th>Product Class</th>
                                            <th>Product Number</th>
                                            <th>Safety Stock Level</th>
                                            <th>Standard Cost</th>
                                            <th>Days To Manufacture</th>
                                             <th>Unit Price</th>
                                            <th>Discount</th>
                                            <th>Line Total</th>
                                        </tr>
                                    </thead>
                                    <tbpdy>
                                        <tr>
                                            <td colspan="18">
                                               <b style='color:red;'> Error, Please  Contact Administrator! </b><br/> 
                                               <b>${errorMessage} </b> <br/>
                                            </td>
                                        </tr>
                                    </tbpdy>
                                </table>
                            </div>
            `;
            $('#data-area').html(tpl);
        }
        else{
            var currentPageIndex = data?.currentPageIndex ?? 0;
            var currentPageSize = data?.currentPageSize ?? 0;
            var maxPageCount = data?.maxPageCount ?? 0;
            var currentPageIndexLabel  = (currentPageIndex < 0 ? 0 : (currentPageIndex > maxPageCount? maxPageCount : currentPageIndex))+1;
            var prevIndex = (currentPageIndex -1< 0? 0 : (currentPageIndex -1));
            var nextIndex = (currentPageIndex+1) > (maxPageCount-1)? (maxPageCount-1): (currentPageIndex+1);
            var totalRows = data?.totalRows ?? 0;
            var ellapsedTime = data?.timeElapsed ?? '';

            var rowTpl = ``;         
            data?.results?.forEach((row,idx,list) => {
                var nomer = (currentPageIndex*currentPageSize)+ idx+1;
                rowTpl += `
                             <tr class='data-row'>
                                <td  style='background-color:gray;'>${nomer}.</td>
                                <td style='text-align:center;'>${row?.salesOrderDetailID}</td>
                                <td style='text-align:center;'>${row?.salesOrderID}</td>
                                <td> ${row?.orderQty?? 0} </td>
                                <td> ${row?.productName??''}  </td>
                                <td> ${row?.productSize??''}  </td>
                                <td> ${row?.productWeight??''}  </td>
                                <td> ${row?.color??''}  </td>
                                <td> ${row?.class??''} </td>
                                <td> ${row?.productNumber??''}  </td>
                                <td> ${row?.safetyStockLevel?? 0} </td>
                                <td> ${row?.standardCost?? 0}  </td>
                                <td> ${row?.daysToManufacture?? 0}  </td>
                                <td> ${row?.unitPrice?? 0} </td>
                                <td> ${row?.unitPriceDiscount?? 0} </td>
                                <td> ${row?.lineTotal?? 0} </td>
                            </tr>
                
                `;
            });

            var tpl = `
                            <div class="col-md-12 row">
                                <div class="col-md-3">
                                    <a class="btn btn-warning btn-sm" onclick="gotoPage(${currentPageIndex})">
                                        Refresh
                                    </a>
                                    &nbsp;
                                    <a class="btn btn-warning btn-sm" onclick="gotoPage(${prevIndex})">
                                        &lt;
                                    </a>
                                    &nbsp;
                                    <a class="btn btn-warning btn-sm" onclick="gotoPage(${nextIndex})">
                                        &gt;
                                    </a>
                                </div>
                                <div class="col-md-3">
                                    <span><b>Page ${currentPageIndexLabel}/${(maxPageCount)} Total Row ${totalRows}</b></span>
                                </div>
                                <div class="col-md-3">
                                    <span><b>Loading time : ${ellapsedTime} milisecond</b></span>
                                </div>
                            </div>
                            <div class="col-md-12 row">
                                <table border="0" width="100%" style="border:1px solid black;">
                                    <thead>
                                        <tr>
                                            <th>No.</th>
                                            <th>Order Detail ID</th>
                                            <th>Order  ID</th>
                                            <th>Order Qty</th>
                                            <th>Product Name</th>
                                            <th>Product Size</th>
                                            <th>Product Weight</th>
                                            <th>Product Color</th>
                                            <th>Product Class</th>
                                            <th>Product Number</th>
                                            <th>Safety Stock Level</th>
                                            <th>Standard Cost</th>
                                            <th>Days To Manufacture</th>
                                             <th>Unit Price</th>
                                            <th>Discount</th>
                                            <th>Line Total</th>
                                        </tr>
                                    </thead>
                                    <tbpdy>
                                        ${rowTpl}
                                    </tbpdy>
                                </table>
                            </div>
            `;
            $('#data-area').html(tpl);
        }
    }


</script>
<script type="text/javascript">
    $(function(){
        gotoPage(0);
    });
</script>

<div class="text-center">
    <h1 class="display-4">Welcome</h1>
    <p>Learn about <a href="https://bayuprn.com">building Web apps with ASP.NET Core</a>.</p>
</div>

<div class="col-md-12 row" style="overflow-x:scroll;">
    <div id="data-area" style="min-width:1800px !important;">
        <div class="col-md-12 row">
            <div class="col-md-3">
                <a class="btn btn-warning btn-sm" onclick="gotoPage(0)">
                    Refresh
                </a>
                &nbsp;
                <a class="btn btn-warning btn-sm" onclick="gotoPage(0)">
                    &lt;
                </a>
                &nbsp;
                <a class="btn btn-warning btn-sm" onclick="gotoPage(0)">
                    &gt;
                </a>
            </div>
            <div class="col-md-3">
                <span><b>Page 0/0 Total Row 0</b></span>
            </div>
            <div class="col-md-3">
                <span><b>Loading time : </b></span>
            </div>
        </div>
        <div class="col-md-12 row">
            <table border="0" width="100%" style="border:1px solid black;">
                <thead>
                    <tr>
                        <th>No.</th>
                        <th>Order Detail ID</th>
                        <th>Order  ID</th>
                        <th>Order Qty</th>
                        <th>Product Name</th>
                        <th>Product Size</th>
                        <th>Product Weight</th>
                        <th>Product Color</th>
                        <th>Product Class</th>
                        <th>Product Number</th>
                        <th>Safety Stock Level</th>
                        <th>Standard Cost</th>
                        <th>Days To Manufacture</th>
                        <th>Unit Price</th>
                        <th>Discount</th>
                        <th>Line Total</th>
                    </tr>
                </thead>
                <tbpdy>

                </tbpdy>
            </table>
        </div>
    </div>
</div>


Setelah kita buat action di controller dan view untuk menampilkan data, kita coba running aplikasi. Tampilan grid terdapat tombol paging ke next page ataupun previous page. Terdapat juga tombol refresh untuk me-refresh tampilan/me-load ulang data. Bagian terpenting di sini adalah keterangan Loading Time, yang menunjukkan waktu pengambilan data. Untuk pengambilan data kita batasi hanya dari awal action berjalan sampai berhasil mengambil data dari SQL Server, kita tidak menghitung faktor latensi dari jaringan meskipun kita saat ini masih running di localhost, karena kita benar benar ingin mengetahui lama running dari eksekusi EF Core ini.

Gambar 4: Tampilan halaman web halaman index yang berhasil mengambil data secara ajax ke GetListOrder dan menampilkan informasi daftar order

Menggunakan metode berbeda

Selanjutnya kita membuat Action yang berbeda tetapi masih di Controller HomeController. Kita buat action dengan nama “GetListOrderV2” , namun pada Action yang kita buat kali ini, kita tidak memanggil stored procedure SP_GetOrderDetail. Alih – alih memanggil stored procedure, kita memanggil query yang equivalent dengan stored procedure tersebut. Kita tidak menggunakan method ToList() di awal untuk mendapatkan hasil object, tetapi kita memanggil method AsQueryable() sepanjang proses filtering dan paging, dan baru memanggil method ToList() di akhir ketika sudah siap ditampilkan.

public IActionResult GetListOrderV2(int pageIndex = 0, int pageSize = 10)
{
    var startTime = DateTime.Now;
    try
    {
        using (var dbContext = new SalesDbContext())
        {
            var sql = $@"
                                SELECT o.[SalesOrderID]
                                      ,o.[SalesOrderDetailID]
                                      ,o.[CarrierTrackingNumber]
                                      ,o.[OrderQty]
                                      ,o.[ProductID]
                                      ,p.[Name] as [ProductName]
                                      ,p.Size as [ProductSize]
                                      ,p.Weight as [ProductWeight]
                                      ,p.Color
                                      ,p.Class
                                      ,p.ProductNumber
                                      ,p.SafetyStockLevel
                                      ,p.StandardCost
                                      ,p.DaysToManufacture
                                      ,o.[SpecialOfferID]
                                      ,o.[UnitPrice]
                                      ,o.[UnitPriceDiscount]
                                      ,o.[LineTotal]
                                      ,o.[rowguid]
                                      ,o.[ModifiedDate]
                                  FROM [AdventureWork2022].[Sales].[SalesOrderDetail] o
                                  left  join [AdventureWork2022].[Production].[Product] p ON o.ProductID=p.ProductID
                        ";

            var queryableResult = dbContext.Database.SqlQueryRaw<OrderDetailVM>(sql).AsQueryable();

            int countTotal = queryableResult.Count();
            int maxPage = countTotal / pageSize + 1;
             var queryable = queryableResult.Skip(pageIndex * pageSize).Take(pageSize).AsQueryable();
             var pagedresult = queryable.ToList();


            var endTime = DateTime.Now;
             var elapsedTime = (endTime - startTime).TotalMilliseconds;

            return this.Json(new
            {
                IsSuccess = true,
                Message = "Success",
                Results = pagedresult,
                CurrentPageIndex = pageIndex,
                CurrentPageSize = pageSize,
                TotalRows = countTotal,
                MaxPageCount = maxPage,
                TimeElapsed = elapsedTime
            });

        }
    }
    catch (Exception ex)
    {
        return this.Json(new
        {
            IsSuccess = false,
            Message = $"Error : {ex.Message} {ex.InnerException?.Message}"
        });
    }
}

Kita ubah fungsi javascript “gotoPage(idx)” yang ada pada Index.cshtml supaya url untuk POST Ajax mengarah ke GetListOrderV2. Perhatikan perubahan pada fungsi JS gotoPage berikut.

function gotoPage(idx){
    var pageIndex = idx<0?0:idx;
    var pageSize = 10;
    $.ajax({
        url: `@Url.Action("GetListOrderV2", "Home")?&pageIndex=${pageIndex}&pageSize=${pageSize}`,
        dataType: 'json',
        method: "POST",
        success: function (result, status, xhr) {
            if (result != null ) {
                renderGrid(result);
                return;
            }
            else {
                renderGrid(null, e);
                return;
            }
        },
        error: function (e) {
            renderGrid(null, e);
            return;
        },
    });
}

Kita coba running ulang aplikasi dan menampilkan data. Tidak ada perubahan tampilan maupun jumlah data yang ditampilkan. Akan tetapi yang perlu dicatan di sini adalah informasi loading time yang ditampilkan pada grid. Kita perlu mencatan lama loading time EF Core setiap kali kita refresh grid maupun berpindah ke paging halaman berikutnya.

Gambar 5: Tampilan halaman web halaman index yang berhasil mengambil data secara ajax ke GetListOrderV2 dan menampilkan informasi daftar order, loading time menampilkan waktu yang lebih cepat

PENGUKURAN LAMA WAKTU PROSES EF CORE

Kita memiliki dua metode, metode pertama dari GetListOrder kita beri nama metode 1.Stored Procedure ToList(), dan metode kedua dari GetListOrderV2 kita beri nama metode 2.Iqueryable. Aplikasi kita jalankan di PC dengan spek processor intel i7 4GHz dan ram 32GB. Pengambilan data diambil dari waktu loading time masing – masing metode hingga 15 kali pengambilan. Pengambilan pertama adalah menggunakan tombol Refresh pada grid yang akan mengambil ulang data. Pengambilan data selanjutnya dengan membuka paging halaman selanjutnya hingga page ke-14. Kita memang tidak mengambil angka ketika pertama kali halaman langsung tampil saat debugging dikarenakan load pertama setelah aplikasi dijalankan pertama kali biasanya sangat berat karena asp.net pertama kali melakukan load seluruh library dan aplikasi sehingga kurang merepresentasikan performa dari EF. Alangkah baiknya setelah halaman tampil pertama kali, kita refresh menggunakan browser refresh terlebih dahulu, baik saat menggunakan metode 1 maupun metode 2.

Gambar 6: Tabel hasil pengukuran performa EF Core antara metode 1 (Stored Procedure+ToList()) dan metode 2 (IQueryable)

Dari hasil pengukuran, metode 1 yang menggunakan Stored Procedure dan method ToList() membutuhkan waktu sekitar 500milisecond – 200 milisecond untuk mengambil 10 baris data ke dalam list object, dengan rata rata didapat sekitar 284 mili second. Keseluruhan total row yang ada di basis data SQL Server adalah sebesar 121317 baris data dan hanya perlu menampilkan 10 baris data sesuai paging. Kemudian untuk metode ke 2 menggunakan IQueryable dimana kita menggunakan query SQL langsung dan memanggil method AsQueryable(), membutuhkan waktu sekitar 50milisecond – 10 milisecond untuk mengambil 10 baris data ke dalam list object, dengan rata – rata didapat sekitar 21 mili second.

Gambar7: Grafik yang menampilkan hasil pengukuran Performa penggunaan stored procedure vs iqueryable

Dari hasil perbandingan tersebut, dapat kita katakan bahwa menggunakan metode IQueryable dimana kita memanggil query dan memanggil method AsQueryable adalah lebih cepat dibanding kita menggunakan metode penggunaan Stored Procedure dan memanggil ToList().

KENAPA LEBIH CEPAT?

Dalam teknik ORM dimana terjadi mapping antara object dengan hasil query database relational, kuncinya adalah Query SQL apa yang dikirimkan ke basis data dan seberapa besar data hasil query perlu dimaterialisasi ke dalam object.

  1. GetListOrder: Ketika kita memanggil stored procedure, EF Core mengirimkan query exec stored procedure ke SQL Server dan mengembalikan 121317 baris data. Dari 121317 ini EF Core perlu menghasilkan 121317 object dalam bentuk sebuah List<OrderDetailVM>. Setelah list List<OrderDetailVM> terbentuk di memory Application Server, barulah dilakukan paging dan filtering menggunakan Linq , pada kode bagian (spResult.Skip(pageIndex * pageSize).Take(pageSize)) di mana dipanggil method Skip() dan Take() untuk mem-paging dan mem-filter list supaya dihasilkan 10 baris data saja.
  2. GetListOrderV2: Ketika kita menggunakan SQL Query dan menggunakan method AsQueryable(), EF Core belum mengirimkan apapun ke SQL Server. Bahkan hingga kita memanggil Skip dan Take pada bagian kode: queryableResult.Skip(pageIndex * pageSize).Take(pageSize).AsQueryable() belum ada query dikirimkan ke SQL Server. EF Core akan menyusun expression tree sehingga dibentuk query SQL akhir lengkap dengan paging tanpa campur tangan programmer. Barulah ketika method ToList() dipanggil, query akhir dikirimkan ke SQL Server dan dihasilkan 10 baris data dari SQL Server. 10 data ini kemudian dibuat oleh EF Core ke dalam 10 object di dalam List<OrderDetailVM> di memory Application Server.

Jika kita lakukan debugging pada method action GetListOrderV2 kita bisa mengintip query SQL yang dihasilkan dari expression tree LINQ sebelum dikirimkan ke SQL Server.

Gambar 8: Debugging pada Visual Studio memungkinkan untuk mengintip query SQL yang dihasilkan oleh expression tree dari Linq to SQL EF Core

Berikut ini adalah SQL Query yang dihasilkan pada action GetListOrderV2 apabila kita mengintip menggunakan fitur debuggin pada Visual Studio, query tersebut terbentuk otomatis oleh EF Core tanpa campur tangan kita dan sesuai dengan alur kode C# yang kita buat.

DECLARE @__p_1 int = 0;
DECLARE @__p_2 int = 10;

SELECT [w].[CarrierTrackingNumber], [w].[Class], [w].[Color], [w].[DaysToManufacture], [w].[LineTotal], [w].[ModifiedDate], [w].[OrderQty], [w].[ProductID], [w].[ProductName], [w].[ProductNumber], [w].[ProductSize], [w].[ProductWeight], [w].[SafetyStockLevel], [w].[SalesOrderDetailID], [w].[SalesOrderID], [w].[SpecialOfferID], [w].[StandardCost], [w].[UnitPrice], [w].[UnitPriceDiscount], [w].[rowguid]
FROM (

                                            SELECT o.[SalesOrderID]
                                                  ,o.[SalesOrderDetailID]
                                                  ,o.[CarrierTrackingNumber]
                                                  ,o.[OrderQty]
                                                  ,o.[ProductID]
    	                                          ,p.[Name] as [ProductName]
    	                                          ,p.Size as [ProductSize]
    	                                          ,p.Weight as [ProductWeight]
    	                                          ,p.Color
    	                                          ,p.Class
    	                                          ,p.ProductNumber
    	                                          ,p.SafetyStockLevel
    	                                          ,p.StandardCost
    	                                          ,p.DaysToManufacture
                                                  ,o.[SpecialOfferID]
                                                  ,o.[UnitPrice]
                                                  ,o.[UnitPriceDiscount]
                                                  ,o.[LineTotal]
                                                  ,o.[rowguid]
                                                  ,o.[ModifiedDate]
                                              FROM [AdventureWork2022].[Sales].[SalesOrderDetail] o
                                              left  join [AdventureWork2022].[Production].[Product] p ON o.ProductID=p.ProductID
                                    
) AS [w]
ORDER BY (SELECT 1)
OFFSET @__p_1 ROWS FETCH NEXT @__p_2 ROWS ONLY

Lantas pada GetListOrder, mengapa kita tidak menggunakan AsQueryable saat memanggil stored procedure dan membiarkan EF Core mengatur query nya? Jawabannya adalah karena Select * from (exec Stored_Procedure() ) adalah sesuatu hal yang invalid. Mari kita coba buktikan dengan merubah source code pada bagian GetListOrder

public IActionResult GetListOrder(int pageIndex=0 , int pageSize = 10)
{
    var startTime = DateTime.Now;
    try
    {
        using (var dbContext = new SalesDbContext())
        {
            var sql = $"exec SP_GetOrderDetail ";

            //var spResult = dbContext.Database.SqlQueryRaw<OrderDetailVM>(sql).ToList();
            // bagian ini akan error
            var spResult = dbContext.Database.SqlQueryRaw<OrderDetailVM>(sql).AsQueryable();

            int countTotal = spResult.Count();
            int maxPage = countTotal / pageSize+1;
            var pagedresult = spResult.Skip(pageIndex * pageSize).Take(pageSize).ToList();


            var endTime = DateTime.Now;
            var elapsedTime = (endTime - startTime).TotalMilliseconds;

            return this.Json(new
            {
                IsSuccess = true,
                Message = "Success",
                Results = pagedresult,
                CurrentPageIndex = pageIndex,
                CurrentPageSize= pageSize,
                TotalRows = countTotal,
                MaxPageCount = maxPage,
                TimeElapsed = elapsedTime
            });

        }
    }
    catch (Exception ex)
    {
        return this.Json(new
        {
            IsSuccess = false,
            Message = $"Error : {ex.Message} {ex.InnerException?.Message}"                    
        });
    }
}

Apabila kita jalankan ulang perubahan source code tersebut, kita akan mendapati exception pada kode dengan pesan yang berbunyi kurang lebih:

‘FromSql’ or ‘SqlQuery’ was called with non-composable SQL and with a query composing over it. Consider calling ‘AsEnumerable’ after the method to perform the composition on the client side.

Gambar 9: Aplikasi Error dengan pesan error yang menunjukkan bahwa FromSql’ or ‘SqlQuery’ was called with non-composable SQL

Pesan exception tersebut menginformasikan bahwa query exec stored procedure bukanlah suatu composable query SQL yang dapat diakses dengan select * from ….. , sehingga mengharuskan kita untuk langsung memanggil ToList() atau method AsEnumerable() dan melakukan paging, filtering, maupun sorting di sisi client side atau dengan kata lain di sisi memory Application Server.

Gambar 10: Unsupported query ketika menggunakan select untuk memanggil exec stored procedure

,


Leave a Reply

Your email address will not be published. Required fields are marked *