kikki's tech note

技術ブログです。UnityやSpine、MS、Javaなど技術色々について解説しています。

ASP.NET Core + Dapper で高パフォーマンスWEB開発を実践する

本章では、2016年6月27日に発表されたASP.NET Core1.0とMicro-ORM MapperのDapperを利用して、高パフォーマンスなクロスプラットフォームWEB開発を実践します。

はじめに

Microsoftは、以下のアナウンスの通り、オープンソースとして開発されたWEB開発プラットフォーム「ASP.NET Core」を発表しました。
blogs.msdn.microsoft.com

ASP.NET Coreは、従来のASP.NET MVCのパフォーマンスを遥かに凌駕します。
github.com
web.ageofascent.com

プレーンテキストの表示のパフォーマンス結果では、ASP.NET MVCが1秒間に約58,000リクエスト処理能力に対して、ASP.NET Coreでは1秒間に約313,000リクエスト処理能力の結果となっています。同条件で、Node.jsが約127,000リクエスト、Scalaが約176,000リクエストと、他のプラットフォームと比較しても、問題ないパフォーマンス結果です。
今までパフォーマンスが悪いとされてきたASP.NET系ですが、ASP.NET Coreの登場によってこれから様々な場面でASP.NETフレームワークが利活用されるのではと楽しみで仕方ありません。

Dapper

Dapperは、超軽量級のDAOです。
github.com

Entity Frameworkは、エンティティを主軸として考えられ、WEB開発者にとって優しく作られています。一方で、Dapperは、SQLを主軸として、パフォーマンスを重要視して作られています。Dapperでは、SQLを直接書くことが基本となりますので、パフォーマンスの調整も容易です。パフォーマンスを重点的に考えるのであれば、DAOの機構としてDapperは採用の候補の一つに挙がるかと思います。
詳しい使い方については公式や、他のサイトを参考にしてください。

開発の準備

まず最新のVisualStudioを用意します。以下サイトからダウンロード、インストールができます。
Microsoft Visual Studio ホームページ - Visual Studio
そしてプロジェクトテンプレートから「ASP.NET Core Web Application」を選択し、プロジェクトを新規に作成します。
f:id:kikkisnrdec:20161025095803p:plain
プロジェクトの準備ができたら、NuGetパッケージで「Dapper」をプロジェクトに加えます。
f:id:kikkisnrdec:20161025100401p:plain
そして以下のライブラリも合わせて追加します。

開発の実践

構成

全体のファイル構成と役割について確認します。

  • Startup.cs
    • 設定ファイルの読み込み
    • 依存性注入(DI)
  • appsettings.json
    • DBへの接続文字列の設定
  • Controllers/HomeController.cs
    • デフォルトアクセス先URL
  • Models/Users.cs
    • ユーザ情報を表すモデル・エンティティ
  • Services/IUsersRepository.cs
  • Services/UsersRepository.cs

エンティティ・モデル

まずDBに、以下クエリでユーザ情報を格納するUsersテーブルを作ります。

CREATE TABLE Users(
 Id uniqueidentifier CONSTRAINT NN__Users__Id NOT NULL
 ,CONSTRAINT PK__Users__Id PRIMARY KEY CLUSTERED (Id ASC) WITH (STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF) 
 ,Name nvarchar(256) CONSTRAINT NN__Users__Name NOT NULL CONSTRAINT UQ__Users__Name UNIQUE(Name)
) ;

次にDBのデータを操作するためのモデルを用意します。BaseEntityは、リポジトリクラスでDBを操作する際に利用します。

    public abstract class BaseEntity
    {
    }
    public class Users : BaseEntity
    {
        /// <summary>
        /// ユーザID
        /// </summary>
        [Key]
        public Guid Id { get; set; }

        /// <summary>
        /// ログイン名(ユーザ名)
        /// </summary>
        [Required]
        public string Name { get; set; }
    }

そしてリポジトリクラスを用意します。

    public interface IRepository<T1, T2> where T1 : BaseEntity
    {
        /// <summary>
        /// レコードを追加します
        /// </summary>
        /// <param name="item"></param>
        /// <returns></returns>
        void Add(T1 item);

        /// <summary>
        /// レコードを削除します
        /// </summary>
        /// <param name="id"></param>
        void Remove(T2 id);

        /// <summary>
        /// レコードを更新します
        /// </summary>
        /// <param name="item"></param>
        bool Update(T1 item);

        /// <summary>
        /// 任意のレコードを取得します
        /// </summary>
        /// <param name="id"></param>
        /// <returns></returns>
        T1 FindByID(T2 id);

        /// <summary>
        /// 有効なレコードを全取得します
        /// </summary>
        /// <returns></returns>
        IEnumerable<T1> FindAll();
    }
    public class UsersRepository : IUsersRepository<Users, Guid?>
    {
        internal IDbConnection Connection
        {
            get
            {
                return new SqlConnection(connectionString);
            }
        }
        public UsersRepository(IConfiguration configuration)
        {
            connectionString = configuration.GetConnectionString("DefaultConnection");
        }

        public void Add(Users item)
        {
            using (IDbConnection db = Connection)
            {
                db.Open();
                using (var tran = db.BeginTransaction())
                {
                    try
                    {
                        db.Execute(@"INSERT INTO [Users] " +
                            "([Id],[Name]) " +
                            "VALUES (NEWID(), @Name)",
                            new
                            {
                                Name = item.Name
                            },
                            tran);
                        tran.Commit();
                    }
                    catch
                    {
                        tran.Rollback();
                    }
                }
            }
        }

        public IEnumerable<Users> FindAll()
        {
            using (var db = Connection)
            {
                db.Open();
                return db.Query<Users>("SELECT * FROM [Users]");
            }
        }

        public Users FindByID(Guid? id)
        {
            if (id == null)
            {
                return null;
            }
            using (IDbConnection db = Connection)
            {
                db.Open();
                return db.Query<Users>("SELECT * FROM [Users] WHERE [Id] = @Id",
                    new { Id = id.Value }
                    ).FirstOrDefault();
            }
        }        

        public void Remove(Guid? id)
        {
            if (id == null)
            {
                return;
            }
            using (IDbConnection db = Connection)
            {
                db.Open();
                using (var tran = db.BeginTransaction())
                {
                    try
                    {
                        db.Execute(@"DELETE FROM [Users] " +
                            "WHERE [Id] = @Id",
                            new
                            {
                                Id = id.Value
                            },
                            tran);

                        tran.Commit();
                    }
                    catch
                    {
                        tran.Rollback();
                        throw;
                    }
                }
            }
        }

        public bool Update(Users item)
        {
            using (IDbConnection db = Connection)
            {
                db.Open();
                using (var tran = db.BeginTransaction())
                {
                    try
                    {
                        var count = db.Execute(@"UPDATE [Users] " +
                            "SET " +
                            "[Name] = @Name" +
                            "WHERE " +
                            "[Id] = @Id",
                            new
                            {
                                Id = item.Id,
                                Name = item.Name
                            },
                            tran);

                        tran.Commit();
                        return count > 0;
                    }
                    catch
                    {
                        tran.Rollback();
                        throw;
                    }
                }
            }
        }
    }

DI

このままでは、リポジトリクラスを使う際に、常に設定ファイルを読み込み、オブジェクトを生成する必要があります。そこで、設定ファイル読み込みをシングルトンで生成し、リポジトリを生成します。
【Startup.cs】

    public class Startup
    {
        public static IConfigurationRoot Configuration { get; private set; }

        public Startup(IHostingEnvironment env)
        {
            // appsettings.jsonファイルの読み込み
            var builder = new ConfigurationBuilder()
                .SetBasePath(env.ContentRootPath)
                .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
                .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true)
                .AddEnvironmentVariables();
            Configuration = builder.Build();
        }

        public void ConfigureServices(IServiceCollection services)
        {
            // MVCを利用できるように調整
            var mvc = services.AddMvc();

            // Configurationをシングルトン化
            services.AddSingleton<IConfiguration>(Configuration);

            // DAOをインスタンス化
            services.AddScoped<IUsersRepository<Users, Guid?>, UsersRepository>();
        }

        public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
        {
            // ルーティングを設定
            app.UseMvc(routes =>
            {
                routes.MapRoute(
                    name: "default",
                    template: "{controller=Home}/{action=Index}/{param?}");
            });
        }
    }

なお設定ファイルは以下の通りです。
【appsettings.json

{
  "ConnectionStrings": {
    "DefaultConnection": "Data Source=localhost;Initial Catalog=HogeHoge;User ID=HogeUser;Password=hogehogePassword;Trusted_Connection = true;Integrated Security = true;"
  }
}

コントローラー

コントローラーでリポジトリを使用する方法について確認します。

    public class HomeController : Controller
    {
        private IUsersRepository<Users, Guid?> usersRepository;

        public HomeController(IUsersRepository<Users, Guid?> _usersRepository)
        {
            usersRepository = _usersRepository;
        }

        public ActionResult Index()
        {
            return View("index");
        }

        public ActionResult FindUser(string id)
        {
            var user = usersRepository.FindById(name);
            //
            // ユーザを検索してhogehogeする
            //
            return null;
        }
    }

筆休め

本章では、Microsoft最新のWEBアプリケーション開発フレームワークASP.NET Coreの実践方法について確認しました。実際のサービスでは、ロギングやエラー処理、セッション管理、認証処理といった多数のサービスを作る必要があるかと思います。それらについても機会があれば、記事にしてみたいと思います。

以上、「ASP.NET Core + Dapper で高パフォーマンスWEB開発を実践する」でした。


※無断転載禁止 Copyright (C) kikkisnrdec All Rights Reserved.