심재운 블로그

반응형

 

이 기사에서는 ASP.NET Core Web API 프로젝트에서 Dapper를 사용하는 방법을 배울 것입니다. Dapper에 대해 전반적으로, 다양한 쿼리와 실행을 사용하는 방법, 저장 프로시저를 실행하는 방법, 트랜잭션 내에서 여러 쿼리를 생성하는 방법에 대해 이야기할 것입니다. 컨트롤러 내부에서 Dapper 쿼리를 직접 사용하는 것을 피하기 위해 로직을 래핑하는 간단한 리포지토리 레이어를 만들 것입니다.

 

 

이 기사의 소스 코드를 다운로드하려면 Dapper with ASP.NET Core Web API 리포지토리를 방문하세요 .

 

 

이 기사를 다음 섹션으로 나눌 것입니다.

 

 

 

자, 시작합시다.

 

Dapper 정보

 

Dapper는 ORM(Object-Relational Mapper) 또는 더 정확하게는 Micro ORM으로 프로젝트의 데이터베이스와 통신하는 데 사용할 수 있습니다. Dapper를 사용하면 SQL Server에서 하는 것처럼 SQL 문을 작성할 수 있습니다. Dapper는 .NET에서 작성한 쿼리를 SQL로 변환하지 않기 때문에 성능이 뛰어납니다. 매개변수화된 쿼리를 사용할 수 있기 때문에 Dapper가 SQL Injection 에 안전하다는 사실을 아는 것이 중요하며, 이것이 우리가 항상 해야 하는 일입니다. 한 가지 더 중요한 것은 Dapper가 여러 데이터베이스 공급자를 지원한다는 것입니다. ADO.NET의 IDbConnection을 확장하고 데이터베이스를 쿼리하는 유용한 확장 메서드를 제공합니다. 물론 데이터베이스 공급자와 호환되는 쿼리를 작성해야 합니다.

 

이러한 확장 메서드에 대해 이야기할 때 Dapper는 동기 및 비동기 메서드 실행을 모두 지원한다고 말해야 합니다. 이 기사에서는 이러한 메서드의 비동기 버전을 사용할 것입니다.

 

 

Extension Methods 에 대해서...

 

Dapper는 다음과 같은 다양한 방법으로 IDbConnection 인터페이스를 확장한다.

  • Execute – 명령을 한 번 또는 여러 번 실행하고 영향을 받는 행 수를 반환하는 데 사용하는 확장 방법
  • Query – 이 확장 방법을 사용하여 쿼리를 실행하고 결과를 매핑할 수 있음
  • QueryFirst – 쿼리를 실행하고 첫 번째 결과를 매핑함
  • QueryFirstOrDefault – 시퀀스에 요소가 없는 경우 쿼리를 실행하고 첫 번째 결과 또는 기본값을 매핑하기 위해 이 방법을 사용한다.
  • QuerySingle – 쿼리를 실행하고 결과를 매핑할 수 있는 확장 방법. 시퀀스에 정확히 하나의 요소가 없을 경우 예외를 발생시킨다.
  • QuerySingleOrDefault – 쿼리를 실행하고 결과를 매핑하거나 시퀀스가 비어 있는 경우 기본값을 매핑하십시오. 시퀀스에 둘 이상의 요소가 있는 경우 예외를 발생시킨다.
  • QueryMultiple – 동일한 명령 및 맵 결과 내에서 여러 쿼리를 실행하는 확장 방법

 

 

우리가 말했듯이 Dapper는 이러한 모든 메서드(ExecuteAsync, QueryAsync, QueryFirstAsync, QueryFirstOrDefaultAsync, QuerySingleAsync, QuerySingleOrDefaultAsync, QueryMultipleAsync)에 대한 비동기 버전을 제공합니다.

 

 

데이터베이스 및 웹 API 생성 및 Dapper 설치

 

프로젝트에서 Dapper를 사용하기 전에 데이터베이스를 준비하고 새 Web API 프로젝트를 만들어야 합니다. 그럼 데이터베이스부터 시작하겠습니다.

 

우리가 할 첫 번째 일은 새 ASPNetCoreDapper데이터베이스 를 만드는 것 입니다. 데이터베이스 생성 후 소스 코드 리포지토리로 이동하여 두 개의 테이블을 생성하고 데이터로 채울  수 있는 스크립트(Data.sql이 있는 초기 스크립트) 를 찾을 수 있습니다.

 

 

 

마이그레이션을 생성하는 방법과 Dapper를 사용하여 데이터를 시드하는 방법을 배우고 싶다면 
FluentMigrator 및 ASP.NET Core를 사용한 Dapper 마이그레이션 문서를 읽을 수 있습니다 .

 

테이블과 데이터가 있으면 새 Web API 프로젝트를 만들 수 있습니다.

 

 

프로젝트가 준비되면 두 가지 필수 패키지를 설치할 수 있습니다.

  • Dapper – PM> Install-Package Dapper -Version 2.0.90
  • SQL Client – PM> Install-Package Microsoft.Data.SqlClient -Version 2.1.2

 

리포지토리 패턴 생성을 계속 진행할 수 있습니다.

 

 

Repository Pattern 만들기

 

이 섹션에서는 간단한 repository pattern 을 만들 것이다. 이 기사는 모두 Dappe r에 관한 것이므로 간단하게 만들 것입니다.

 

더보기

완전한 리포지토리 패턴 을 만드는 방법을 배우고 싶다면 해당 주제에 대한 기사를 읽을 수 있습니다. 또한 그런 식으로 작성하려는 경우 비동기 버전을 찾을 수 있습니다 .

 

즉, Entities내부에 두 개의 클래스가 있는 새 폴더를 만드는 것으로 시작하겠습니다 .

 

public class Employee
{
    public int Id { get; set; }
    public string Name { get; set; }
    public int Age { get; set; }
    public string Position { get; set; }
    public int CompanyId { get; set; }
}

public class Company
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Address { get; set; }
    public string Country { get; set; }
    public List<Employee> Employees { get; set; } = new List<Employee>();
}

 

따라서 이들은 데이터베이스의 테이블을 나타내는 두 가지 모델 클래스입니다.

그런 다음 appsettings.json내부에 연결 문자열을 추가 하여 파일을 수정할 수 있습니다 .

 

"ConnectionStrings": {
    "SqlConnection": "server=.; database=DapperASPNetCore; Integrated Security=true"
  },

 

물론 필요에 맞게 연결 문자열을 자유롭게 수정하십시오. 이제 새 Context폴더와 그 DapperContext아래에  클래스 를 만들 것입니다.

 

public class DapperContext
{
    private readonly IConfiguration _configuration;
    private readonly string _connectionString;
    public DapperContext(IConfiguration configuration)
    {
        _configuration = configuration;
        _connectionString = _configuration.GetConnectionString("SqlConnection");
    }
    public IDbConnection CreateConnection()
        => new SqlConnection(_connectionString);
}

 

appsettings.json 파일에서 connection string 를 접근할 수 있도록 하기 위해 IConfiguration interface 를 inject 합니다.

new SQLConnection  객체를 반환하는 CreateConnection 메소드를 생성합니다.

 

이것이 작동하려면 몇 가지 using 문을 추가해야 합니다

 

using Microsoft.Data.SqlClient;
using Microsoft.Extensions.Configuration;
using System.Data;

 

클래스 생성 후 Startup 클래스에 singleton 서비스로 등록할 수 있습니다 .

 

public void ConfigureServices(IServiceCollection services)
{
    services.AddSingleton<DapperContext>();
    services.AddControllers();
}

 

Repository Interfaces 및 Class

 

다음으로 새 Contracts 폴더와 그 안에 단일 interface 를 만들 것입니다.

 

public interface ICompanyRepository
{
}

 

또한 새 Repository폴더와 그 안에 단일 클래스를 만들어 보겠습니다 .

public class CompanyRepository : ICompanyRepository
{
    private readonly DapperContext _context;
    public CompanyRepository(DapperContext context)
    {
        _context = context;
    }
}

 

Dapper 쿼리 작업을 시작하면 이 두 파일을 모두 채울 것입니다.

마지막으로 인터페이스와 구현을 Startup클래스 의 서비스로 등록해 보겠습니다 .

 

public void ConfigureServices(IServiceCollection services)
{
    services.AddSingleton<DapperContext>();
    services.AddScoped<ICompanyRepository, CompanyRepository>();
    services.AddControllers();
}

 

ASP.NET Core Web API 에서 Dapper Queries 사용

 

 

데이터베이스에서 모든 companies  를 반환하는 예부터 시작하겠습니다.

따라서 가장 먼저 하고 싶은 일은 ICompanyRepository 인터페이스 를 수정하는 것입니다 .

 

public interface ICompanyRepository
{
    public Task<IEnumerable<Company>> GetCompanies();
}

 

그런 다음 CompanyRepository 클래스 에서 이 메서드를 구현해 보겠습니다 .

 

public async Task<IEnumerable<Company>> GetCompanies()
{
    var query = "SELECT * FROM Companies";
    using (var connection = _context.CreateConnection())
    {
        var companies = await connection.QueryAsync<Company>(query);
        return companies.ToList();
    }
}

 

 

따라서 query모든 회사를 가져오기 위해 SQL 쿼리를 저장할 문자열 변수를 만듭니다 . 그런 다음 using명령문  에서 메서드 를 호출하여 DapperContext개체를 사용하여 개체를 만듭니다 SQLConnection(또는 더 정확하게는 IDbConnection개체) CreateConnection. 보시다시피 연결 사용을 중단하는 즉시 폐기해야 합니다. 연결을 생성하면 이를 사용하여 QueryAsync메서드 를 호출 하고 쿼리를 인수로 전달할 수 있습니다. 때문에 QueryAsync메소드가 반환 IEnumerable<T>, 우리는 즉시 우리가 결과를 반환하기 원하는 목록으로 변환합니다.

 

QueryAsync메서드 에서 강력한 형식의 결과를 사용한다는 점에 주목하는 것이 중요합니다 : QueryAsync<Company>(query). 그러나 Dapper는 익명 결과도 지원합니다 connection.QueryAsync(query). . 

 

예제에서는 강력한 형식의 결과를 사용할 것입니다.

 

 

API Controller Logic

 

이제 새로 CompaniesController만들고 수정해 보겠습니다 .

 

 

[Route("api/companies")]
[ApiController]
public class CompaniesController : ControllerBase
{
    private readonly ICompanyRepository _companyRepo;
    public CompaniesController(ICompanyRepository companyRepo)
    {
        _companyRepo = companyRepo;
    }
    [HttpGet]
    public async Task<IActionResult> GetCompanies()
    {
        try
        {
            var companies = await _companyRepo.GetCompanies();
            return Ok(companies);
        }
        catch (Exception ex)
        {
            //log error
            return StatusCode(500, ex.Message);
        }
    }
}

 

여기에서 DI를 통해 저장소를 주입하고 이를 사용하여 GetCompanies메서드 를 호출합니다 .

 

여기에 몇 가지 참고 사항:

  • 어떤 종류의 비즈니스 로직도 없기 때문에 저장소 계층을 래핑하기 위해 서비스 계층을 생성하지 않습니다. 이러한 유형의 애플리케이션의 경우 서비스 계층은 리포지토리 메서드만 호출하고 문서에 불필요한 수준의 복잡성을 추가합니다. 물론 대규모 애플리케이션에서는 항상 서비스 계층을 사용하는 것이 좋습니다.
  • 예제를 위해 컨트롤러의 각 작업에서 try-catch 블록을 사용할 것입니다. 그러나 코드 반복을 피하기 위해 Global Error Handling 기사를 읽는 것이 좋습니다.
  • 컨트롤러의 로직은 설명하지 않을 것이며 Web API 개발에 익숙하다고 가정합니다. 이에 대해 자세히 알아보려면 ASP.NET Core Web API 시리즈를 참조 하세요.

이제 앱을 시작하고 테스트할 수 있습니다.

 

 

 

다른 속성 및 열 이름 (Different Property and Column Names)

 

현재 Company 클래스의 모든 속성  Companies 테이블 내부의 열과 이름이 같습니다. 그러나 그것들이 일치하지 않으면 어떻게 될까요? 확인 해보자.

 

먼저 Company 클래스 안에 Name 내부의 속성을 수정합니다 .

 

public class Company
{
    public int Id { get; set; }
    public string CompanyName { get; set; }
    public string Address { get; set; }
    public string Country { get; set; }
    public List<Employee> Employees { get; set; } = new List<Employee>();
}

 

 

이제 동일한 요청을 실행하면 다른 결과를 얻게 됩니다.

 

보시다시피 companyName 속성은 null입니다. 이는 Dapper 가 query를 작성한 방식으로 매핑할 수 없기 때문입니다.

따라서 별칭을 사용하여 GetCompanies 메서드 내부의 query 를 수정해 보겠습니다 .

 

public async Task<IEnumerable<Company>> GetCompanies()
{
    var query = "SELECT Id, Name AS CompanyName, Address, Country FROM Companies";
    using (var connection = _context.CreateConnection())
    {
        var companies = await connection.QueryAsync<Company>(query);
        return companies.ToList();
    }
}

 

보시다시피 AS 키워드를 사용하여 Name열의 별칭을 만들고 있습니다 .

이제 Postman에서 동일한 요청을 보낼 수 있습니다.

 

 

매핑이 완벽하게 작동하는 것을 볼 수 있습니다.

이제 미래의 모든 쿼리에서 별칭을 쓰지 않도록 모두 이전 상태로 되돌려 보겠습니다.

 

 

Dapper 쿼리와 함께 매개변수 사용 (Using Parameters with Dapper Queries)

 

이 기사의 시작 부분에서 말했듯이 Dapper는 매개변수화된 쿼리를 지원하므로 100% SQL Injection 안전합니다. anonymous, dynamic, list, string, and table-valued parameters 를 지원합니다. 이 기사에서는 주로 anonymous parameters dynamic 를 사용할 것입니다.

 

즉, 인터페이스 수정부터 시작하겠습니다.

 

 

public interface ICompanyRepository
{
    public Task<IEnumerable<Company>> GetCompanies();
    public Task<Company> GetCompany(int id);
}

 

그런 다음 CompanyRepository 클래스에 메서드 구현을 추가해 보겠습니다 .

 

public async Task<Company> GetCompany(int id)
{
    var query = "SELECT * FROM Companies WHERE Id = @Id";
    using (var connection = _context.CreateConnection())
    {
        var company = await connection.QuerySingleOrDefaultAsync<Company>(query, new { id });
        return company;
    }
}

 

이 방법은 이전 방법과 거의 동일하지만 QuerySingleOrDefaultAsync 를 사용하고, 두 번째 인수로 anonymous object 를 제공 하기 때문에 한 가지 예외가 있습니다 . 데이터베이스에 new Company 엔터티를 만드는 다음 dynamic parameters 를 사용하는 방법을 보여 드리겠습니다.

 

다음으로 controller 를 수정해야 합니다.

 

[HttpGet("{id}", Name = "CompanyById")]
public async Task<IActionResult> GetCompany(int id)
{
    try
    {
        var company = await _companyRepo.GetCompany(id);
        if (company == null)
            return NotFound();
        return Ok(company);
    }
    catch (Exception ex)
    {
        //log error
        return StatusCode(500, ex.Message);
    }
}

 

Postman으로 이것을 테스트할 수 있습니다.

 

 

 

 

Execute(Async) 메서드를 사용하여 데이터베이스에 새 Entity 추가하기

 

이제 API에서 POST 요청을 처리하고 ExecuteAsync메서드를 사용하여 데이터베이스에 새 회사 엔터티를 만들 것입니다.

가장 먼저 할 일은 새 Dto폴더 를 만들고 그 안에 CompanyForCreationDtoPOST 요청에 사용할  클래스를 만드는 것입니다.

 

public class CompanyForCreationDto
{
    public string Name { get; set; }
    public string Address { get; set; }
    public string Country { get; set; }
}

 

이 DTO를 사용하는 이유(그리고 업데이트 작업에 다른 DTO를 사용할 예정)에 대해 자세히 알아보려면 ASP.NET Core Web API 시리즈 기사를 참조하세요. 여기에서 그 이유를 설명합니다(기사 시리즈의 5  6 ).

 

이 클래스를 만든 후 인터페이스를 수정합니다.

 

public interface ICompanyRepository
{
    public Task<IEnumerable<Company>> GetCompanies();
    public Task<Company> GetCompany(int id);
    public Task CreateCompany(CompanyForCreationDto company);
}

 

물론 저장소 클래스에서 이 메서드를 구현해 보겠습니다.

 

public async Task CreateCompany(CompanyForCreationDto company)
{
    var query = "INSERT INTO Companies (Name, Address, Country) VALUES (@Name, @Address, @Country)";
    var parameters = new DynamicParameters();
    parameters.Add("Name", company.Name, DbType.String);
    parameters.Add("Address", company.Address, DbType.String);
    parameters.Add("Country", company.Country, DbType.String);
    using (var connection = _context.CreateConnection())
    {
        await connection.ExecuteAsync(query, parameters);
    }
}

 

 

여기에서 쿼리와 동적 매개변수 개체를 만듭니다(더 이상 익명 개체를 사용하지 않음). 해당 객체를 세 개의 매개변수로 채운 다음, ExecuteAsync메서드를 호출하여 insert 문을 실행합니다. 

 

ExecuteAsync방법은 반환 int 데이터베이스에 영향을 받는 행의 수를 나타내는, 그 결과로. 따라서 해당 정보가 필요한 경우 이 메서드에서 반환 값을 수락하여 사용할 수 있습니다.

 

이제 이 메서드를 호출하고 생성할 회사를 전달하면 새 엔터티가 생성됩니다. 그러나 API의 POST 작업을 생성하는 동안 API 사용자가 생성된 엔터티를 탐색하는 데 사용할 수 있는 링크를 반환하는 것이 좋습니다. 

 

Ultimate ASP.NET Core Web API  에서 이에 대해 자세히 설명합니다 . 이 방법에 있는 코드를 사용하면 쉽게 그렇게 할 수 없습니다. 따라서 몇 가지를 수정해야 합니다.

 

Ultimate ASP.NET Core Web API - Code Maze

With the Ultimate ASP.NET Core Book, you are going to learn how to create a full production ready Web API with the Docker and Security implemented.

code-maze.com

 

 

더 나은 API 솔루션 만들기

 

먼저 인터페이스를 수정해 보겠습니다.

 

public interface ICompanyRepository
{
    public Task<IEnumerable<Company>> GetCompanies();
    public Task<Company> GetCompany(int id);
    public Task<Company> CreateCompany(CompanyForCreationDto company);
}

 

 

이번에는 생성된 company 엔터티를 반환하는 메서드를 원합니다.

다음으로 메서드 구현을 수정해야 합니다.

 

public async Task<Company> CreateCompany(CompanyForCreationDto company)
{
    var query = "INSERT INTO Companies (Name, Address, Country) VALUES (@Name, @Address, @Country)" +
        "SELECT CAST(SCOPE_IDENTITY() as int)";
    var parameters = new DynamicParameters();
    parameters.Add("Name", company.Name, DbType.String);
    parameters.Add("Address", company.Address, DbType.String);
    parameters.Add("Country", company.Country, DbType.String);
    using (var connection = _context.CreateConnection())
    {
        var id = await connection.QuerySingleAsync<int>(query, parameters);
        var createdCompany = new Company
        {
            Id = id,
            Name = company.Name,
            Address = company.Address,
            Country = company.Country
        };
        return createdCompany;
    }
}

 

 

현재 범위에서 생성된 최종 identity 값을 반환하기 위해 SELECT 구문을 추가하여 쿼리를 정정합니다. 

그런 다음 QuerySingleAsync 메서드 를 호출하여 해당 id 값을 추출합니다. 

이 메서드는 INSERT및 SELECT문을 둘다 실행합니다. 

 

Id 값이 있으면 필수 필드를 사용하여 new Company 개체를 생성하기만 하면 됩니다. 물론 이것은 원하는 매핑 도구로 할 수 있는 작업입니다. 마지막으로 생성된 엔터티를 반환합니다.

 

 

이를 통해 컨트롤러에 POST 작업을 추가할 수 있습니다.

 

[HttpPost]
public async Task<IActionResult> CreateCompany(CompanyForCreationDto company)
{
    try
    {
        var createdCompany = await _companyRepo.CreateCompany(company);
        return CreatedAtRoute("CompanyById", new { id = createdCompany.Id }, createdCompany);
    }
    catch (Exception ex)
    {
        //log error
        return StatusCode(500, ex.Message);
    }
}

 

보시다시피 데이터베이스에 new company 를 만든 후 새로 만든 엔터티를 가져올 수 있는 경로를 반환합니다.

테스트해 보겠습니다.

 

 

 

우리의 new company 가 있습니다.

또한 응답의 헤더 탭을 검사하면 이 company 에 대한 URI를 찾을 수 있습니다.

 

 

업데이트 및 삭제 작업 (Working with Update and Delete)

 

우리는 이미 필요한 모든 지식을 가지고 있기 때문에 업데이트 및 삭제 작업은 이제 매우 간단합니다. 그럼 바로 코드로 넘어가겠습니다.

우리는 새로운 DTO로 시작할 것입니다:

 

public class CompanyForUpdateDto
{
    public string Name { get; set; }
    public string Address { get; set; }
    public string Country { get; set; }
}

 

 

그런 다음 인터페이스를 수정해 보겠습니다.

public interface ICompanyRepository
{
    public Task<IEnumerable<Company>> GetCompanies();
    public Task<Company> GetCompany(int id);
    public Task<Company> CreateCompany(CompanyForCreationDto company);
    public Task UpdateCompany(int id, CompanyForUpdateDto company);
    public Task DeleteCompany(int id);
}

 

다음으로 저장소 클래스에서 이 두 가지 메서드를 구현해야 합니다.

 

public async Task UpdateCompany(int id, CompanyForUpdateDto company)
{
    var query = "UPDATE Companies SET Name = @Name, Address = @Address, Country = @Country WHERE Id = @Id";
    var parameters = new DynamicParameters();
    parameters.Add("Id", id, DbType.Int32);
    parameters.Add("Name", company.Name, DbType.String);
    parameters.Add("Address", company.Address, DbType.String);
    parameters.Add("Country", company.Country, DbType.String);
    using (var connection = _context.CreateConnection())
    {
        await connection.ExecuteAsync(query, parameters);
    }
}
public async Task DeleteCompany(int id)
{
    var query = "DELETE FROM Companies WHERE Id = @Id";
    using (var connection = _context.CreateConnection())
    {
        await connection.ExecuteAsync(query, new { id });
    }
}

 

보시다시피 이 두 가지 방법에는 새로운 것이 없습니다. 쿼리, 매개변수가 있고 ExecuteAsync 메서드로 명령문을 실행합니다 .

마지막으로 컨트롤러에 두 가지 작업을 추가해야 합니다.

 

 

[HttpPut("{id}")]
public async Task<IActionResult> UpdateCompany(int id, CompanyForUpdateDto company)
{
    try
    {
        var dbCompany = await _companyRepo.GetCompany(id);
        if (dbCompany == null)
            return NotFound();
        await _companyRepo.UpdateCompany(id, company);
        return NoContent();
    }
    catch (Exception ex)
    {
        //log error
        return StatusCode(500, ex.Message);
    }
}
[HttpDelete("{id}")]
public async Task<IActionResult> DeleteCompany(int id)
{
    try
    {
        var dbCompany = await _companyRepo.GetCompany(id);
        if (dbCompany == null)
            return NotFound();
        await _companyRepo.DeleteCompany(id);
        return NoContent();
    }
    catch (Exception ex)
    {
        //log error
        return StatusCode(500, ex.Message);
    }
}

 

PUT 및 DELETE 요청을 모두 전송하여 Postman으로 이를 테스트할 수 있습니다.

 

 

 

 

Dapper로 저장 프로시저 실행하기

 

 

Dapper를 사용하여 저장 프로시저를 호출하는 방법을 보여주기 전에 데이터베이스에 하나를 생성해야 합니다.

 

USE [DapperASPNetCore]
GO
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE PROCEDURE [dbo].[ShowCompanyForProvidedEmployeeId] @Id int
AS
  SELECT c.Id, c.Name, c.Address, c.Country
  FROM Companies c JOIN Employees e ON c.Id = e.CompanyId
  Where e.Id = @Id
GO

 

이 절차는 제공된 Id 값을 가진 직원이 있는 회사의 이름, 주소 및 국가를 반환합니다.

저장 프로시저가 있으면 인터페이스 수정으로 넘어갈 수 있습니다.

 

public interface ICompanyRepository
{
    public Task<IEnumerable<Company>> GetCompanies();
    public Task<Company> GetCompany(int id);
    public Task<Company> CreateCompany(CompanyForCreationDto company);
    public Task UpdateCompany(int id, CompanyForUpdateDto company);
    public Task DeleteCompany(int id);
    public Task<Company> GetCompanyByEmployeeId(int id);
}

 

그런 다음 클래스를 수정해 보겠습니다.

public async Task<Company> GetCompanyByEmployeeId(int id)
{
    var procedureName = "ShowCompanyForProvidedEmployeeId";
    var parameters = new DynamicParameters();
    parameters.Add("Id", id, DbType.Int32, ParameterDirection.Input);
    using (var connection = _context.CreateConnection())
    {
        var company = await connection.QueryFirstOrDefaultAsync<Company>
            (procedureName, parameters, commandType: CommandType.StoredProcedure);
        return company;
    }
}

 

여기에서 프로시저 이름과 내부에 단일 매개 변수가 있는 동적 매개 변수 개체를 포함하는 변수를 만듭니다. 

저장 프로시저가 값을 반환하기 때문에 QueryFirstOrDefaultAsync 메서드를 사용하여 값 을 실행합니다. 

저장 프로시저가 값을 반환하지 않으면 ExecuteAsync 메서드를 실행에 사용할 수 있다는 점에 유의하십시오 .

 

평소와 같이 컨트롤러에 다른 작업을 추가해야 합니다.

 

[HttpGet("ByEmployeeId/{id}")]
public async Task<IActionResult> GetCompanyForEmployee(int id)
{
    try
    {
        var company = await _companyRepo.GetCompanyByEmployeeId(id);
        if (company == null)
            return NotFound();
        return Ok(company);
    }
    catch (Exception ex)
    {
        //log error
        return StatusCode(500, ex.Message);
    }
}

 

 

우리 프로젝트에서 Dapper로 저장 프로시저를 호출하는 것이 얼마나 쉬운지 알 수 있습니다.

 

 

Single Query 로 여러 SQL 문 실행 (Executing Multiple SQL Statements with a Single Query)

 

여러 SQL 문을 쉽게 실행할 수 있으며 QueryMultipleAsync 메서드 를 사용하여 단일 쿼리에서 여러 결과를 반환할 수 있습니다. 

 

예를 들어 어떻게 하는지 봅시다.

항상 그렇듯이 인터페이스를 먼저 수정합니다.

 

public interface ICompanyRepository
{
    ...
    public Task<Company> GetCompanyEmployeesMultipleResults(int id);
}

 

public async Task<Company> GetCompanyEmployeesMultipleResults(int id)
{
    var query = "SELECT * FROM Companies WHERE Id = @Id;" +
                "SELECT * FROM Employees WHERE CompanyId = @Id";
    using (var connection = _context.CreateConnection())
    using (var multi = await connection.QueryMultipleAsync(query, new { id }))
    {
        var company = await multi.ReadSingleOrDefaultAsync<Company>();
        if (company != null)
            company.Employees = (await multi.ReadAsync<Employee>()).ToList();
        return company;
    }
}

 

 

보시다시피 쿼리 변수에는 두 개의 SELECT명령문이 포함되어 있습니다 . 첫 번째는 단일 회사를 반환하고 두 번째는 해당 회사의 모든 직원을 반환합니다. 그런 다음 연결을 만든 다음 해당 연결을 사용하여 QueryMultipleAsync 메서드 를 호출합니다 . multi 변수 내에서 여러 결과를 얻으면 ReadSignleOrDefaultAsync  ReadAsync 메서드 를 사용하여 두 결과(해당 회사 및 직원)를 모두 추출할 수 있습니다 . 

 

첫 번째 메서드는 단일 결과를 반환하고 두 번째 메서드는 컬렉션을 반환합니다.

 

컨트롤러에서 액션을 생성하기만 하면 됩니다.

 

[HttpGet("{id}/MultipleResult")]
public async Task<IActionResult> GetCompanyEmployeesMultipleResult(int id)
{
    try
    {
        var company = await _companyRepo.GetCompanyEmployeesMultipleResults(id);
        if (company == null)
            return NotFound();
        return Ok(company);
    }
    catch (Exception ex)
    {
        //log error
        return StatusCode(500, ex.Message);
    }
}

 

 

우리는 거기에 갈. 한 회사에 소속된 모든 직원을 볼 수 있습니다.

 

 

다중 매핑 (Multiple Mapping)

 

이전 예에서는 두 개의 SQL 문을 사용하여 두 개의 결과를 반환한 다음 단일 개체에서 함께 결합했습니다. 그러나 일반적으로 이러한 쿼리의 경우 두 개의 SQL 문을 작성하고 싶지 않고 JOIN 절을 사용하여 단일 SQL 문을 만들고 싶습니다. 물론 그렇게 쓰면 QueryMultipleAsync 더 이상  방법을 사용할 수 없다 . 

 

우리는 잘 알려진 QueryAsync 방법 으로 다중 매핑 기술(multiple mapping technique) 을 사용해야 합니다 . 그래서, 우리가 그것을 할 수 있는 방법을 봅시다. 평소와 같이 인터페이스 수정부터 시작하겠습니다.

 

public interface ICompanyRepository
{
    ...
    public Task<List<Company>> GetCompaniesEmployeesMultipleMapping();
}

 

 

public async Task<List<Company>> GetCompaniesEmployeesMultipleMapping()
{
    var query = "SELECT * FROM Companies c JOIN Employees e ON c.Id = e.CompanyId";
    using (var connection = _context.CreateConnection())
    {
        var companyDict = new Dictionary<int, Company>();
        var companies = await connection.QueryAsync<Company, Employee, Company>(
            query, (company, employee) =>
            {
                if (!companyDict.TryGetValue(company.Id, out var currentCompany))
                {
                    currentCompany = company;
                    companyDict.Add(currentCompany.Id, currentCompany);
                }
                currentCompany.Employees.Add(employee);
                return currentCompany;
            }
        );
        return companies.Distinct().ToList();
    }
}

 

 

따라서 쿼리를 만들고 using 문 내부에서 새 connection 을 만듭니다. 그런 다음 companies 를 유지하기 위해 새 dictionary 을 만듭니다. 데이터베이스에서 데이터를 추출하기 위해 이 QueryAsync방법을 사용하고 있지만 이번에는 지금까지 본 적이 없는 새로운 구문이 있습니다. 

 

세 가지 제네릭 유형을 볼 수 있습니다. 

 

처음 두 가지는 우리가 작업할 입력 유형이고 세 번째는 반환 유형입니다. 이 메서드는 쿼리를 매개 변수로 수락하고 Company 그리고 Employee 유형의 두 매개 변수를 수락하는 Func delegate 도 수락합니다. delegate 내부에서 Id 값으로 companies 를 추출하려고 합니다. 존재하지 않으면 currentCompany 변수 내부에 저장 하고 dictionary 에 추가합니다. 또한 모든 직원을 현재 companies 에 할당하고 결과적으로 Func delegate 로부터 이를 반환합니다.

 

mapping 이 완료되면 목록으로 변환된 고유한 결과를 반환합니다.

 

컨트롤러에 작업을 추가하기만 하면 됩니다.

 

[HttpGet("MultipleMapping")]
public async Task<IActionResult> GetCompaniesEmployeesMultipleMapping()
{
    try
    {
        var company = await _companyRepo.GetCompaniesEmployeesMultipleMapping();
        return Ok(company);
    }
    catch (Exception ex)
    {
        //log error
        return StatusCode(500, ex.Message);
    }
}

 

 

그리고 우리는 그것이 매력처럼 작동하는 것을 볼 수 있습니다.

 

처리해야 할 것이 하나 더 남아 있습니다. 바로 transactions 입니다.

 

 

Transactions

 

트랜잭션은 Dapper와 함께 사용하는 것이 매우 간단합니다. Dapper 라이브러리(이미 사용하고 있는 라이브러리 )를 사용하거나 Dappr.Transaction 라이브러리 를 사용하여 실행할 수 있습니다. 이는 기본적으로 확장 IDbConnection 인터페이스만 있는 Dapper와 동일한 것 입니다. 이 예에서는 Dapper 라이브러리를 사용할 것입니다.

 

트랜잭션을 구현하는 저장소 메소드만 보여드리겠습니다. 이 기사에서 단계를 여러 번 반복했기 때문에 나머지는 모두 매우 간단합니다.

 

public async Task CreateMultipleCompanies(List<CompanyForCreationDto> companies)
{
    var query = "INSERT INTO Companies (Name, Address, Country) VALUES (@Name, @Address, @Country)";
    using (var connection = _context.CreateConnection())
    {
        connection.Open();
        using (var transaction = connection.BeginTransaction())
        {
            foreach (var company in companies)
            {
                var parameters = new DynamicParameters();
                parameters.Add("Name", company.Name, DbType.String);
                parameters.Add("Address", company.Address, DbType.String);
                parameters.Add("Country", company.Country, DbType.String);
                await connection.ExecuteAsync(query, parameters, transaction: transaction);
            }
            transaction.Commit();
        }
    }
}

 

여기 4가지 추가 사항이 있습니다. 지금까지 보지 못한 것입니다. 먼저 connection open 해야 합니다. 그런 다음 using 문 내에서 BeginTransaction 메서드 를 호출하여 트랜잭션을 시작합니다 . 

 

ExecuteAsync 메소드에서, transaction 을 지정합니다. 마지막으로 트랜잭션을 커밋하는 Commit 메서드를 호출합니다 .

 

오류를 시뮬레이션하고 데이터베이스에 행이 생성되지 않는지 테스트하려면, await 코드 줄 바로 아래에 exception 를 throw 하면 됩니다. 그러면 Companies  테이블에서 새로운 행의 데이터를 찾을 수 없습니다.

 

 

 

결론

 

 

ASP.NET Core 프로젝트에서 Dapper를 통합하는 방법과 Dapper에서 쿼리, 실행, 저장 프로시저 및 트랜잭션을 사용하는 방법을 배웠습니다.

 

https://code-maze.com/using-dapper-with-asp-net-core-web-api/

 

Using Dapper with ASP.NET Core Web API - Code Maze

Let's learn how to use Dapper in ASP.NET Core Web API by using different querys, executions, transactions and repository pattern.

code-maze.com

 

반응형

댓글

비밀글모드