مقایسه و عملکرد Dapper ، Entity Framework و ADO.NET

ما در گروه خود پروژه‌ای داریم که کارایی آن بسیار مهم است. این پروژه کوئری‌های ما را در داده‌های پایگاه داده SQL درگیر می‌کند، داده‌ها را به اشیای strongly-typed تبدیل کرده و سپس این اشیاء را به یک سیستم تماس از طریق لایه سرویس بازمی‌گرداند. درنهایت این سرویس ستون مرکزی در معماری سرویس‌گرای سازمان (SOA) ما خواهد شد و به همین دلیل باید سریع باشد.

مقایسه و عملکرد Dapper ، Entity Framework  و ADO.NET

ما معمولا می‌خواهیم از Entity Framework برای ORM خود استفاده کنیم،‌ اما با کمی جستجو در سؤالات StackExchange و توضیحات وبلاگ‌ها معلوم می‌شود که چگونه EF واقعا برای سیستم‌های با کارایی بالا چندان رضایت‌بخش نیست. این شکاف با مراحلی که "micro-ORMs" نامیده می‌شوند مثل Dapper.NET پر می‌شود که عملکرد مربوط به قابلیت نگهداری را به عهده می‌گیرد.

چون عملکرد در این برنامه بسیار مهم است، ما می‌خواهیم مطمئن شویم کدام یک از این ORMها بهترین شرایط را برای ما فراهم می‌کنند. بنابراین ما روی یک پروژه نمونه بر روی GitHub کار کردیم که هر یک از این سه روش دسترسی به داده را می‌گیرد تا آن‌ها را با استفاده از داده‌ها و کوئری‌های مشابه (با برخی ملاحظات، همان‌طور که در زیر مشاهده می‌کنید) امتحان کند. این مقاله به بخش‌های زیر تقسیم می‌شود:

1. متدولوژی

2. تنظیم تست

3. نتایج

4. تحلیل

5. نتیجه‌گیری

متدولوژی

این تست از یک ساختار پایگاه داده‌ای استفاده می‌کند که به صورت زیر است:

به عبارت دیگر، ورزش (Sport) تعدادی تیم (Team) دارد و یک تیم تعدادی بازیکن (Player) دارد.

ما نیاز به تعدادی داده برای تست داریم. پروژه نمونه یک بخش کامل اختصاص داده شده به تولید این داده‌ها را دارد، فقط کافی است بگویید با چه شیوه‌ای می‌توانید تعدادی ورزش، تعدادی تیم برای هر ورزش، و تعدادی بازیکن برای هر تیم در هر تست را انتخاب کنید.

آنچه که اکنون ما نیاز داشتیم مجموعه‌ای از کوئری‌ها بود که باید در هر ORM ایجاد کرده و آن را تست می‌کردیم. ما سه کوئری مختلف را انتخاب کردیم:

بازیکن توسط ID

بازیکن‌های هر تیم

تیم‌های هر ورزش (ازجمله بازیکنان)

برای هر کوئری، تست را برای همه داده‌های پایگاه داده اجرا خواهیم کرد (مثلا برای بازیکن توسط ID، هر بازیکن را با ID خودش انتخاب می‌کنیم) و به طور میانگین کل زمانی که برای اجرای کوئری صرف می‌شود (ازجمله تنظیم DbContext یا SqlConnection) را برای هر اجرا در نظر می‌گیریم. سپس چندین اجرا از این مورد را برای داده‌های مشابه انجام می‌دهیم تا بتوانیم میانگین اجرای آن‌ها را محاسبه کرده و به طور واضح نشان دهیم که کدام ORM سریع‌تر است.

تنظیم تست

به عنوان مثال، در اینجا کدی برای کلاس‌های تست Entity Framework، ADO.NET و Dapper.NET وجود دارد:

public class EntityFramework : ITestSignature
{
    public long GetPlayerByID(int id)
    {
        Stopwatch watch = new Stopwatch();
        watch.Start();
        using (SportContext context = new SportContext())
        {
            var player = context.Players.Where(x => x.Id == id).First();
        }
        watch.Stop();
        return watch.ElapsedMilliseconds;
    }

    public long GetPlayersForTeam(int teamId)
    {
        Stopwatch watch = new Stopwatch();
        watch.Start();
        using (SportContext context = new SportContext())
        {
            var players = context.Players.Where(x => x.TeamId == teamId).ToList();
        }
        watch.Stop();
        return watch.ElapsedMilliseconds;
    }

    public long GetTeamsForSport(int sportId)
    {
        Stopwatch watch = new Stopwatch();
        watch.Start();
        using (SportContext context = new SportContext())
        {
            var players = context.Teams.Include(x=>x.Players).Where(x => x.SportId == sportId).ToList();
        }
        watch.Stop();
        return watch.ElapsedMilliseconds;
    }
}

public class ADONET : ITestSignature
{
    public long GetPlayerByID(int id)
    {
        Stopwatch watch = new Stopwatch();
        watch.Start();
        using(SqlConnection conn = new SqlConnection(Constants.ConnectionString))
        {
            conn.Open();
            using(SqlDataAdapter adapter = new SqlDataAdapter("SELECT Id, FirstName, LastName, DateOfBirth, TeamId FROM Player WHERE Id = @ID", conn))
            {
                adapter.SelectCommand.Parameters.Add(new SqlParameter("@ID", id));
                DataTable table = new DataTable();
                adapter.Fill(table);
            }
        }
        watch.Stop();
        return watch.ElapsedMilliseconds;
    }

    public long GetPlayersForTeam(int teamId)
    {
        Stopwatch watch = new Stopwatch();
        watch.Start();
        using(SqlConnection conn = new SqlConnection(Constants.ConnectionString))
        {
            conn.Open();
            using(SqlDataAdapter adapter = new SqlDataAdapter("SELECT Id, FirstName, LastName, DateOfBirth, TeamId FROM Player WHERE TeamId = @ID", conn))
            {
                adapter.SelectCommand.Parameters.Add(new SqlParameter("@ID", teamId));
                DataTable table = new DataTable();
                adapter.Fill(table);
            }
        }
        watch.Stop();
        return watch.ElapsedMilliseconds;
    }

    public long GetTeamsForSport(int sportId)
    {
        Stopwatch watch = new Stopwatch();
        watch.Start();
        using(SqlConnection conn = new SqlConnection(Constants.ConnectionString))
        {
            conn.Open();
            using(SqlDataAdapter adapter = new SqlDataAdapter("SELECT p.Id, p.FirstName, p.LastName, p.DateOfBirth, p.TeamId, t.Id as TeamId, t.Name, t.SportId FROM Player p INNER JOIN Team t ON p.TeamId = t.Id WHERE t.SportId = @ID", conn))
            {
                adapter.SelectCommand.Parameters.Add(new SqlParameter("@ID", sportId));
                DataTable table = new DataTable();
                adapter.Fill(table);
            }
        }
        watch.Stop();
        return watch.ElapsedMilliseconds;
    }
}

public class Dapper : ITestSignature
{
    public long GetPlayerByID(int id)
    {
        Stopwatch watch = new Stopwatch();
        watch.Start();
        using (SqlConnection conn = new SqlConnection(Constants.ConnectionString))
        {
            conn.Open();
            var player = conn.Query<PlayerDTO>("SELECT Id, FirstName, LastName, DateOfBirth, TeamId FROM Player WHERE Id = @ID", new{ ID = id});
        }
        watch.Stop();
        return watch.ElapsedMilliseconds;
    }

    public long GetPlayersForTeam(int teamId)
    {
        Stopwatch watch = new Stopwatch();
        watch.Start();
        using (SqlConnection conn = new SqlConnection(Constants.ConnectionString))
        {
            conn.Open();
            var players = conn.Query<List<PlayerDTO>>("SELECT Id, FirstName, LastName, DateOfBirth, TeamId FROM Player WHERE TeamId = @ID", new { ID = teamId });
        }
        watch.Stop();
        return watch.ElapsedMilliseconds;
    }

    public long GetTeamsForSport(int sportId)
    {
        Stopwatch watch = new Stopwatch();
        watch.Start();
        using (SqlConnection conn = new SqlConnection(Constants.ConnectionString))
        {
            conn.Open();
            var players = conn.Query<PlayerDTO, TeamDTO, PlayerDTO>("SELECT p.Id, p.FirstName, p.LastName, p.DateOfBirth, p.TeamId, t.Id as TeamId, t.Name, t.SportId FROM Team t "
                + "INNER JOIN Player p ON t.Id = p.TeamId WHERE t.SportId = @ID", (player, team) => { return player; }, splitOn: "TeamId", param: new { ID = sportId });
        }
        watch.Stop();
        return watch.ElapsedMilliseconds;
    }
}

توجه داشته باشید که در مورد Dapper.NET و ADO.NET، یک سطر را برای هر بازیکن در کوئری GetTeamsForSport انتخاب خواهیم کرد. این یک مقایسه دقیق در برابر کوئری EF نیست، اما برای هدفی که ما داریم به خوبی کار می‌کند.

نتایج

نتایج زیر برای 10 تکرار هستند، هر کدام شامل 8 ورزش، 30 تیم در هر ورزش و 100 بازیکن برای هر تیم می‌باشد.

نتایج Entity Framework

نتایج ADO.NET

نتایج Dapper.NET

تحلیل (Analysis)

همانطور که در داده‌هایی که در بالا ذکر شده است می‌بینید، Entity Framework به طور قابل توجهی کندتر از ADO.NET یا Dapper.NET می‌باشد، که به ترتیب 10-3 برابر کندتر است.

بیایید مسأله را بازتر کنیم: به کوئری "Teams per Sport" توجه کنید؛ در این کوئری Entity Framework هم تیم‌ها در یک ورزش معین و هم بازیکن‌های مربوط به هر تیم (از طریق ()Include) را انتخاب کرد، در حالی که کوئری‌های ADO.NET و Dapper.NET فقط داده‌های پیوست شده (joined data) را انتخاب کردند. در یک مطالعه آماری دقیق‌تر، نتایج بهتری را به دست خواهید آورد.

چیزی که جالب‌تر است این است که Dapper.NET برای کوئری‌های پیچیده‌تر، به طور متوسط سریع‌تر از ADO.NET بود. ما معتقدیم که این موضوع مربوط به این واقعیت است که در مورد تست ADO.NET ما از SqlDataAdapter استفاده می‌کنیم، اگرچه نمی‌توانیم این موضوع را ثابت کنیم.

حتی اگر کوئری "تیم‌های هر ورزش" را هم در نظر نگیریم، هنوز EF حداقل 3 مرتبه کندتر از Dapper.NET و ADO.NET است. داده‌ها نشان می‌دهند که حداقل از لحاظ سرعت و با این کوئری‌ها، Entity Framework آهسته‌ترین گزینه و Dapper.NET سریع‌ترین گزینه خواهد بود. به همین دلیل نتیجه نهایی ما ممکن است شما را شگفت‌زده کند.

نتیجه‌گیری

ما قصد داریم از Dapper.NET روی پروژه خود استفاده کنیم، در این انتخاب شکی نیست، با این حال نمی‌خواهیم توسعه را با آن آغاز کنیم، و تنها ORM مورد استفاده ما نیست. هدف این است که این پروژه را با استفاده از Entity Framework توسعه دهیم، و بعدا با استفاده از Dapper.NET در سناریوی خاصی که سیستم نیاز به افزایش عملکرد دارد پروژه را بهینه‌سازی کنیم. بله، ما با آهسته‌ترین گزینه شروع می‌کنیم. چرا این کار را می‌کنیم؟

زیرا اشکال بزرگ استفاده از Dapper.NET این است که شما در کد خود کوئری‌های عادی SQL را دارید. اگر هر کسی هر اشتباه تایپی را انجام دهد، ما تا زمانی که تست‌های مربوط به کد را اجرا نکنیم از هیچ مشکلی باخبر نمی‌شویم. به علاوه اعضای تیم ما با EF بیشتر از Dapper.NET آشنا هستند، بنابراین زمان توسعه سریع‌تر خواهد بود.

به طور خلاصه، Dapper.NET بدون شک سریع‌تر از EF و کمی سریع‌تر از ADO.NET است، اما اکثرا ما توسعه را با EF انجام داده و در صورت نیاز آن را با Dapper.NET بهینه‌سازی می‌کنیم. ما فکر می‌کنیم این روش توازنی بین سهولت توسعه و عملکرد ایجاد می‌کند (و امیدوارم به ما اجازه دهید از هر دو روش استفاده کرده و آن را به درستی انجام دهیم).

آموزش سی شارپ