ADO.NET과 ORM에 대한 비교의 대한 정보가 필요하던 중 아래 포스팅을 가져오게 되었다.

잊을만하면 한 번씩 읽어보기로 하며 정리.

 

 

ADO.NET vs an ORM (Dapper & EF) | The Machine Spirit

A case for an ORM compared to traditional ADO.NET and/or raw SQL scripts.

www.bensampica.com

Introduction

현재 매일 Entity Framework Core를 사용합니다. 이 주제에 대한 StackOverflow에 대한 질문을 보았고 기존의 ADO.NET 및 SQL 스크립트를 사용하여 ORM(객체 관계형 매퍼)이없이 작업 한 여러 프로젝트에 대해서 많은 대화를 나누며, ORM을 사용하는 것과 비교해 보았습니다. 이 게시물에서는 Entity Framework를 주로 다루었지만, Dapper도 포함한 차이점에 대해서 이야기를 하도록 하겠습니다.

The Theoretical

보안 고려사항(Security Considerations)

Microsoft가 이야기하는 EF에 대한 포괄적인 보안 고려 사항 목록이 있습니다. 다음은 Dapper 또는 EF를 안전하게 사용하기 위해 조직에서 고려해야 할 몇 가지 사항입니다.

ORM Usage

  • 원시 SQL 쿼리 문자열 빌드를 방지하여 외부 사용자 입력에서 SQL 주입을 방지합니다. Dapper와 Entity Framework는 모두 매개변수화된 쿼리를 빌드하고 삭제된 매개변수를 저장 프로시저에 전달하는 방법을 제공하며 가장 드물고 통제되는 경우를 제외하고는 이러한 매개변수를 선호해야 합니다.
  • 매우 큰 결과 세트를 피하십시오. 5백만 개의 결합된 레코드를 메모리로 선택하는 큰 결과 집합으로 인해 응용 프로그램/시스템이 충돌할 수 있습니다. 애플리케이션에 필요한 것만 쿼리합니다.
  • (EF만 해당) - 서비스 계정이 필요 [db_datareader] [db_datawriter]하고 [db_ddladmin]데이터베이스에 권한이 적용됩니다.

Performance Considerations

성능이 뛰어나고 강력하며 확장 가능한 엔터프라이즈 애플리케이션을 구축 및 유지 관리하는 것이 ORM의 핵심입니다. 다음은 100,000개 이상의 레코드와 평균 <10ms의 SQL 응답 시간이 있는 Entity Framework를 사용하는 관계형 데이터베이스 구현의 라이브 예입니다 . 생성되는 쿼리와 속도에 대한 로그를 확인하십시오.

Materializing

Materialization을 이해하는 것은 ORM 사용의 중요한 부분입니다. 가능한 가장 적은 수의 개체를 구체화하는 쿼리를 작성하는 것은 고성능 응용 프로그램을 유지 관리하는 데 중요합니다. EF 및 Dapper에는 쿼리를 메모리로 구체화하는 일반적인 C# 식이 있습니다. 둘 다 그렇게 하는 몇 가지 고유한 방법과 명시적인 호출 없이 자동으로 구체화되는 몇 가지 제약 조건이 있습니다.

Generated SQL (EF Only)

Entity Framework는 구체적으로 Entities SQL(EF 6+ 및 EF Core)이라는 SQL 언어를 작성한 다음 대상 데이터 원본 SQL(MSSQL, MYSQL 등)로 변환합니다. 따라서 애플리케이션 변경 없이 모든 유형의 데이터베이스에 애플리케이션 계층을 이식할 수 있지만 잠재적인 성능 문제가 있는 쿼리를 유지하는 것이 더 쉽기 때문에 더 복잡한 개체에 대한 쿼리를 생성할 때 약간의 생각이 필요합니다.

실용적인 (The Practical)

보안 및 성능 고려 사항을 염두에 두고 다음은 ORM이 실제로 빛을 발하고 애플리케이션 개발 수명 주기가 개선된 몇 가지 범주의 예입니다.

 

직원을 유지 관리하는 응용 프로그램을 고려하십시오. 애플리케이션은 호출된 테이블 dbo.Employee과 domain.EmployeeType다음 속성 을 사용하여 데이터베이스에 연결 합니다.

EmployeeId (int) [PK] EmployeeTypeId (int) [PK]
EmployeeTypeId (int) [FK] Value [nvarchar]
Name (nvarchar) Active (bit)
Active (bit)  

새로운 개발 가속화 (Speeding Up New Development)

새로운 쿼리를 실행하고 새로운 데이터베이스 테이블과 관련 애플리케이션 모델을 지속적으로 추가하는 데 걸리는 시간을 줄이는 것은 ORM의 가장 큰 이점 중 하나입니다.

 

Comparison: Select all columns from all employees

 

ORM (Entity Framework)

public IEnumerable<Employee> GetEmployees() 
{ 
	return _entityFrameworkContext.Employees; 
}

ORM (Dapper)

public IEnumerable<Employee> GetEmployees()
{
    var employeeProperties = typeof(Employee).GetProperties().Select(prop => prop.Name);

    var sqlQuery = new StringBuilder("SELECT ")
        .AppendJoin(", ", employeeProperties)
        .Append($" FROM [dbo].[{nameof(Employee)}]")
        .ToString();

    using (var databaseConnection = new SqlConnection(_applicationOptions.ConnectionString))
    {
        return await databaseConnection.QueryAsync<Employee>(
            sqlQuery,
            commandType: CommandType.Text);
    }
}

 

ADO.NET SQL Script

USE [Employees]
GO
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE PROCEDURE [dbo].[GetEmployees]
SELECT * FROM [dbo].[Employee]
GO
USE [Employees]
GRANT EXECUTE ON OBJECT::[dbo].[GetEmployees]
TO rl_Employee_ServiceAccount;

 

애플리케이션 계층 (Application Layer)

public List<Employee> GetEmployees())
{
    using (var connection = new SqlConnection())
    {
        using (SqlCommand cmd = new SqlCommand("[dbo].[GetEmployees]", connection))
        {
            cmd.CommandType = CommandType.StoredProcedure;
            connection.Open();
            SqlDataReader reader = cmd.ExecuteReader();
            return HydrateData(reader);

        }
    }
}
public List<Employee> HydrateData(SqlDataReader reader)
{
    var data = new List<Employee>();
    using (reader)
    {
        if(reader.HasRows)
        {
              while (reader.Read())
              {
                   data.Add(
                       new Employee()
                       {
                            EmployeeId = reader.GetInt32(0),
                            EmployeeTypeId = reader.GetInt32(1),
                            Name = reader.GetString(2),
                            Active= reader.GetBoolean(3),
                       }
                   );
              }
              reader.Close();
          }
      }
      return data;
}

표시된 바와 같이 이것은 매우 단순한 것에 대해 매우 장황합니다. 가독성이 떨어지고 저장 프로시저에서 응용 프로그램으로의 데이터 흐름을 따를 수 있는 능력이 약해집니다. SQL 디버깅은 훨씬 더 많은 시간이 소요되고 실수하기 쉽습니다.

 

 

이러한 자세한 정보 차이는 쿼리가 더 복잡해짐에 따라 길어집니다. 몇 가지 예:

  • 필터링된 쿼리와 함께 카운트를 수행하려는 것과 같은 일반적인 접선 쿼리는 기존 SQL에서 번거롭고(중복성이 필요하지만) EF에서는 단순히 .Count()입니다. EF의 . Where() 절을 통해 응용 프로그램 내부에서 선택적 매개 변수를 사용하여 쿼리를 작성하는 경우에도 마찬가지입니다.
public Tuple<IEnumerable<Employee>, int> GetEmployeeByTypeAndStatus(int employeeTypeId, bool? isActive)
{
    // 모든 레코드를 가져오거나 선택적 매개 변수와 일치하는 레코드를 선택합니다.
    var allRecords = _entityFrameworkContext.Employees
        .Where(employee => isActive == null || employee.IsActive == isActive);
    // Get the total count of the base query.
    var totalRecordsCount = allRecords.Count();

    // 데이터를 필터링하고 결과를 반환합니다.
    var filteredData = allRecords
        .Where(employee => employee.EmployeeTypeId == employeeTypeId);
    return new { filteredData , totalRecordsCount };
}
  • 레코드를 추가하거나 업데이트하려면 추가 또는 삽입을 분리하는 논리가 있는 하나의 저장 프로시저가 필요하며 거의 전체 명령문이 두 번 복제되거나 별도로 유지 관리되어야 하는 두 개의 별도 저장 프로시저가 필요합니다. 둘 다 여전히 열 불일치의 대상이 됩니다. _entityFrameworkContext.Employees.AddOrUpdate(employee)

이러한 쿼리가 존재하고 직원이 새 열을 받은 경우 필요한 작업의 차이를 상상해 보십시오(그리고 실수할 가능성도 있음).

유지보수 감소(Maintenance Reduction)

이전 예에서 현재 아키텍처 파일은 인덱스별로 바인딩되지 않은 배열을 읽고 있으며 새로운 변경 사항에 매우 취약합니다. 열 유형이 변경되면 어떻게 됩니까? 저장 프로시저가 내부에 새 열을 추가하고 인덱스를 엉망으로 만든다면 어떻게 될까요? 또한 dbo.Employee테이블과 상호 작용 하고 새 정보가 필요한 모든 단일 저장 프로시 저도 업데이트해야 합니다.

ORM을 사용하면 모델에 새 속성을 추가하는 것만큼 쉽고 새 속성을 필요로 하는 전체 응용 프로그램의 모든 단일 쿼리에 즉시 사용할 수 있습니다.

public class Employee
{
    public int EmployeeId { get; set; }
    public int EmployeeTypeId { get; set; }
    public string Name { get; set; }
    public string Alias { get; set; } // 새 속성 추가.
    public bool Active { get; set; }
}

직원 정보를 필요로 하는 유사한 쿼리가 많이 있다고 상상해 보십시오. 애플리케이션이 성장함에 따라 발생하는 데이터베이스 열 변경은 Employee모델 의 속성을 추가/업데이트하기만 하면 됩니다 . 모든 단일 쿼리는 새 정보를 포함하도록 자동으로 업데이트됩니다. 한정자를 통해 특정 매장 정보를 선택하는 모든 쿼리 .Select()는 제대로 변경되지 않은 상태로 유지됩니다.

버그를 줄이기 위해 할 말이 있습니다. 강력한 형식의 모델을 통해 데이터베이스와 응용 프로그램 간의 결합된 관계로 인덱스, 열 형식 불일치 또는 저장 프로시저 결과 집합 불일치로 인한 사고가 없습니다. 수정되는 파일이 적고 오류 가능성이 적어 변경 영향이 줄어듭니다.

 

EF 전용

전역 쿼리 필터링은 실행 쿼리에 추가되는 '미들웨어' 쿼리 역할을 합니다. 엔터티의 일시 삭제를 허용하는 위의 직원 및 직원 유형 테이블을 고려하십시오. 전역 쿼리 필터는 .Where(employee => employee.IsActive)필요한 경우 재정의할 수 있는 모든 쿼리에 추가 할 수 있습니다. 이는 개발자 실수를 방지하여 유지 관리를 줄이고 개발 속도를 높입니다.

소유 엔터티는 상위 개체에 연결된 경우에만 생성되도록 설정할 수 있습니다. 자체 테이블이 필요하지 않은 공통 엔터티 그룹 속성을 줄이는 데 자주 사용됩니다. 아래 모델을 고려하십시오.

public class Employee {
    public int PersonId { get; set; }
    public Name Name { get; set; }
}

public class ExternalNewsletterSubscriber {
    public int ExternalNewsletterSubscriberId { get; set; }
    public Name Name { get; set; }
    public string EmailAddress { get; set; }
}

[Owned]
public class Name {
    public string FirstName { get; set; }
    public string MiddleName { get; set; }
    public string LastName { get; set; }
}​

 

Name클래스는 모두 재사용 Employee과 ExternalNewsletterSubscriber와 Name속성은 데이터베이스에서의 적절한 소유자 테이블에 매핑됩니다. 여기 에 대해 자세히 알아보십시오 .

 

통합 테스트 (Integration Testing)

EF는 복잡한 비즈니스 논리에 대한 통합 테스트를 가능한 한 쉽게 작성할 수 있도록 하는 메모리 내 데이터베이스 기능을 제공합니다. 전통적으로 테스트를 위해 데이터베이스가 필요할 때 SSDT 프로젝트에서 수작업으로 구축했습니다. EF의 메모리 내 데이터베이스를 사용하면 테스트가 실행되는 동안 각 통합 테스트가 고유한 원자성 데이터베이스를 가질 수 있으므로 조롱이나 기존 데이터가 필요 없이 기존 애플리케이션 코드와 원활하게 작업할 수 있습니다.

public class TestsDbContextFixture
{
    public readonly DbContext DbContext { get; set; }

    public TestsDbContextFixture()
    {
        var options = new DbContextOptionsBuilder<DbContext>()
            // 각 테스트에 고유한 "로컬" 데이터베이스가 있도록 메모리 내 데이터베이스 사용.
            .UseInMemoryDatabase(Guid.NewGuid().ToString())
            .Options;
        DbContext = new DbContext (options);
    }
}

public class IntegrationTests
{
      [Fact]
      public void Test_DbContext()
      {
          // 메모리 내 데이터베이스 초기화
          var context = new TestsDbContextFixture().DbContext;
          // 테스트 소스..
      }
}

Dapper는 Moq와 같은 모의 프레임워크 및/또는 SQLLite 또는 SSDT 프로젝트와 같은 가벼운 데이터베이스를 활용하여 쉽게 결합할 수 있습니다.

 

몇 가지 참고 사항 (Some Notes)

보안과 성능을 염두에 둘 때 Entity Framework와 Dapper는 모두 애플리케이션 개발 수명 주기를 개선할 수 있는 엄청난 기회를 제공합니다. 새로운 개발 속도를 높이고 유지 관리 및 버그 수정 시간을 줄이며 강력한 통합 테스트를 제공함으로써 ORM은 기존 및 신규 개발의 애플리케이션 수명 주기에서 크게 고려해야 합니다.

Additional Resources

'C#' 카테고리의 다른 글

C# Single or array JSON converter.  (0) 2021.10.07
C# Task WaitAll 와 WhenAll  (0) 2021.09.16
C# 정규식을 활용한 계좌번호 ,휴대폰 번호 마스킹 처리.  (0) 2021.09.08
c# Entity Framework  (0) 2021.08.29
[C#] Garbage Collection  (0) 2021.07.08

+ Recent posts