[EF Core]Entity Framework core

2022. 3. 13. 02:38TIL💡/Database

✅ Introduction

Entity Framework Core은 가볍고, 확장 가능성이 높은 크로스 플랫폼 버전의 마이크로소프트 ORM이다.

Entity Framework는 공식적인 데이터 액세스 플래솦밍다.

 

✅ Why Entity Framework Core

EF Core은 ORM(Object-Relational Mapper)로 .NET 개발자들이 .NET 오브젝트로 데이터베이스 개발할 수 있도록 한다.

이는 lighweight, extensible하고, 크로스플랫폼적으로 사용될 수 있다.

이를 통해 개발자들이 데이터를 접근하는 코드를 작성할 필요를 줄인다.

 

✅ Basic Query

Entity Framework Core는 LINQ(Language Integrate Query)를 사용해 데이터베이스로부터 데이터를 쿼리한다.

  • LINQ는 C#을 사용해 Derived Context와 Entity Class를 기반으로 작성하도록 한다.
  • 이는 최적화된 SQL 쿼리를 제공하고, C# 함수를 LINQ-to-Entities 쿼리에 포함할 수 있도록 한다.

예시 모델

public class Customer
{
    public int CustomerId { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string Address { get; set; }
    public virtual List<Invoice> Invoices { get; set; }
}

Load All Data

using (var context = new MyContext())
{
	var customers = context.Customers.ToList();
}

Load a Single Entity

 

using (var context = new MyContext())
{
	var customers = context.Customers
    	.Single(c => c.CustomerId == 1);
}

Filter Data

 

using (var context = new MyContext())
{
	var customers = context.Customers
    	.Where(c => c.FirstNAme == "Mark")
        .ToList();
}

✅ Include

Include 메서드는 쿼리 결과를 포함해 연결된(related) 오브젝트들을 구체화한다.

이는 데이터베이스로부터 정보를 가져오고 관련 엔티티를 포함한다.

예시 모델

public class Customer
{
    public int CustomerId { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string Address { get; set; }
    public virtual List<Invoice> Invoices { get; set; }
}

public class Invoice
{
    public int InvoiceId { get; set; }
    public DateTime Date { get; set; }
    public int CustomerId { get; set; }
    [ForeignKey("CustomerId")]
    public Customer Customer { get; set; }
    public List<InvoiceItem> Items { get; set; }
}

public class InvoiceItem
{
    public int InvoiceItemId { get; set; }
    public int InvoiceId { get; set; }
    public string Code { get; set; }
    [ForeignKey("InvoiceId")]
    public virtual Invoice Invoice { get; set; }
}

모든 customers와 관련된 invoices를 불러올 때 Include 메소드를 사용한다.

using(var context = new MyContext())
{
	var customers = context.Customers
    	.Include(i => i.Invoices)
        .ToList();
}

 

 

✅ ThenInclude

Include 메소드는 오브젝트 리스트에 잘 작동하나, 만약 다수의 레벨이 필요한 경우에 어떻게 해야 하는가?

예를 들어, Customer가 invoice 리스트를 포함하고, 각 invoice는 각 item list를 포함한다.

 

EF Core는 ThenInclude() 메소드를 확장해서 가진다.

이를 통해 Relationships를 파고 들어갈 수 있다.

using (var context = new MyContext())
{
	var customers = context.Cusomters
    	.Include(i => i.Invoices)
        	.ThenInclude(it => it.Items)
        .ToList();
}

 

✅ AsNoTracking

Introduction

Tracking behavior는 엔티티 인스턴스에 대한 정보를 추적하는지 여부를 조절한다.

만약 엔티티가 추적되면. 엔티티에 추적되는 어떠한 변화든 saveChanges() 동안 데이터베이스에 지속된다.

 

다음 예시에서 customer address에 대한 변화는 추적될 것이며 saveChanges()동안 데이터베이스에 지속된다.

using(var context = new MyContext())
{
	var customer = context.Customers
    	.Where(c => c.CustomerId == 2)
        .FirstOrDefault();
    customer.Address = "43 rue St.Laurent";
    context.SaveChanges();
}

No-tracking 

No tracking query는 빠르게 실행된다.

왜냐하면 변화 추적에 대한 설정이 필요없기 때문이다.

이는 결과들이 오직 read-only 시나리오를 사용될 때 매우 유용하다.

 

우리는 AsNoTracking() 메소드를 사용해 no-tracking query로 바꿀 수 있다.

 

AsNoTracking

AsNoTracking() 메소드는 change tracker가 반환되는 엔티티 중 어떠한 엔티티도 추적하지 않는 새로운 쿼리를 리턴한다.

만약 엔티티 인스턴스가 수정되면, 이는 change tracker로 추적되지 않고, SaveChanges()는 지속되지 않는다.

using(var context = new MyContext())
{
	var customers = context.Customers
    	.AsNoTracking().ToList();
}

우리는 디폴트 추적 행동을 context 인스턴스 수준에서 바꿀 수도 있다.

using (var context = new MyContext())
{
    context.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
    var customers = context.Customers.ToList();
}

 

PROS & CONS

Pros

  • Improved performance over regular LINQ queries
  • Fully materialized objects
  • Simplest to write with syntax built into the programming language
  • Improve performance of future tracked queries

Cons

  • Not suitable for CUD operation(READ 제외)
  • Certain technical restrictions, such as: Patterns using DefaultIfEmpty for OUTER JOIN queries result in more complex queries than simple OUTER JOIN statements in Entity SQL
  • Doesn't associate related entities already existing in the Change Tracker
  • You still can't use LIKE with general pattern matching

AsNoTracking vs. AutoDetectChangesEnabled

AsNoTracking 메소드는 Disconnected entities를 반환한다.

엔티티를 쿼리할 때 성능이 향상됩니다.

 

AutoDetectChangesEnabled는 자동적으로 수정이 Connected Entities에서 만들어졌는지 확인하지 않는다.

성능은 엔티티가 저장될 때 향상됩니다.

 

✅ Global Filter

Introduction

Entity Framework Core 2.0은 모델이 만들어질 때 엔티티에 적용되는 Global Query Filters를 도입하였다.

  • 이는 모델 수준에 대한 필터를 명시하고, 자동적으로 해당 context에서 실행되는 모든 쿼리에 적용된다.
  • 이는 Entity Framework는 LINQ 쿼리에서 자동적으로 필터를 WHERE 구문에 필터가 추가한다.
  • 주로, Global Query Filters는 context에서 OnModelCreating 메소드에 적용된다.

예시 모델

public class Customer
{
    public int CustomerId { get; set; }
    public string Name { get; set; }
    public bool IsDeleted { get; set; }
}

Global Query Filter는 데이터베이스 컨텍스트가 모델을 세울 수 있을 때 정의된다. 그래서 컨텍스트 클래스 안의 OnModelingCreating 메소드에서 Global Query Filter를 구성하고, HasQueryFilter 메소드를 사용해 엔티티 타입에 Global Filter를 적용한다.

 

public class MyContext : DbContext
{
    public DbSet<Customer> Customers { get; set; }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder.UseSqlServer(@"Data Source=(localdb)\ProjectsV13;Initial Catalog=CustomerDB;");
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Customer>().HasQueryFilter(c => !c.IsDeleted);
    }
}

HasQueryFilter 메소드에 전달되는 expression은 자동적으로 Customer 타입에 대한 모든 LINQ 쿼리에 적용된다.

하나의 레코드가 이미 삭제(deleted)되었고, IsDeleted column이 True인 레코드를 포함하고 있다고 가정해보자.

만약 데이터베이스에 있는 모든 customers를 LINQ 쿼리를 통해 불러올 수 있다.

using (var context = new MyContext())
{
    var customers = context.Customers.ToList();
}

데이터베이스에는 4개의 레코드가 있지만, 우리는 3개의 레코드만 가져온다.왜냐하면 앞서서 Global Filter가 필터링된 레코드를 가지고 있고, 오직 IsDeleted 칼럼의 값이 False인 레코드만 리턴하기 때문이다.

 

일부 경우에는 필터를 적용할 필요가 없을 때는 우리는 각각의 LINQ 쿼리에 IgnoreQueryFilters() 메소드를 써서 필터를 비활성화할 수 있다.

using (var context = new MyContext())
{
    var customers = context.Customers
        .IgnoreQueryFilters().ToList();
}

 

✅ Joining

Introduction

SQL에서 JOIN 구문은 연결된 column을 기반으로 2개 이상의 테이블을 결합할 때 사용된다.Entity Framework Core에서 Join()과 GroupJoin() 메소드를 써서 동일한 결과를 얻을 수 있다.

 

다음 쿼리는 Customers와 Invoices 테이블을 Join() 메소드를 써서 조인 쿼리를 수행할 수 있다.

var query = context.Customers
	.Join(
    	context.Invoices,
        customer => customer.CustomerId,
        invoice => invoice.Customer.CustomerId,
        (customer, invoice) => new 
        {
        	InvoiceId = invoiceId,
            CustomerName = customer.FirstName + "" + customer.LastName,
            InvoiceDate = invoice.Date
        }
       ).ToList();
foreach (var invoice in query)
{
	Console.WriteLine("InvoiceID: {0}, Customer Name: {1} " + "Date: {2} ",
    	invoice.InvoiceId, invoice.CustomerName, invoice.InvoiceDate);
}

다음 쿼리는 각 Invoice마다의 모든 Items를 찾기 위해 Invoices와 InvoiceItems 테이블로 GroupJoin()을 수행한다.

var query = context.Invoices
	.GroupJoin(
    	context.InvoiceItems,
        invoice => invoice,
        item => item.Invoice,
        (invoice, invoiceItems) => 
        	new
            {
            	InvoiceId = invoice.Id,
                Items = invoiceItems.Select(item => item.code)
            }
     ).ToList();

foreach(var obj in query)
{
	Console.WriteLine("{0}:", obj.InvoiceId);
    foreach(var item in obj.Items)
    {
    	Console.WriteLine("  {0}", item);
    }
}

 

✅ Raw SQL Queries

Introduction

Entity Framework Core는 LINQ로 충분히 쿼리를 할 수 없는 상황을 대비해 raw SQL queries를 직접적으로 실행할 수 있도록 한다.

EF Core는 DbSet.FromSql() 메소드로 raw SQL query를 실행하도록 한다.

using(var context = new MyContext())
{
	var customers = context.Customers
    	.FromSql("SELECT * FROM dbo.Customers")
        .ToList();
}

Passing parameters

SQL을 받아들이는 어떤 API든 유저의 입력을 파라미터화(parameterize)해서 SQL Injection Attck을 방어해야한다.

DbSet.FromSql()메소드 역시 C# 내부의 string interpolation 문법을 활용해 파라미터화된 쿼리를 지원한다. 

 

using(var context = new MyContext())
{
	var customers = context.Custmers
    	.FromSql("Select * from dbo.Customers where LastName = '{0}'", firstName)
        .ToList();
}

 

LINQ Operators

Raw Query 후에도 LINQ Operators를 사용할 수 있다.

using (var context = new MyContext())
{
    var customers = context.Customers
        .FromSql("Select * from dbo.Customers where FirstName = 'Andy'")
        .OrderByDescending(c => c.Invoices.Count)
        .ToList();
}

Limitations

  • FromSql()메소드에 명시된 SQL Query는 반드시 모든 속성들을 리턴해야 한다.
  • 결과셋에 있는 column명은 반드시 속성명과 매치해야 한다.
  • SQL Query는 연관 데이터를 포함할 수 없고, Include 메소드 사용되어 연관 데이터까지 모두 리턴하는 쿼리 위에서만 구성할 수 있다.
  • Supplied SQL은 서브쿼리처럼 취급될 수 있으므로 전달받은 SQL이 서브쿼리에 유효하지 않은 문자, 옵션을 포함하지 않도록 해야 한다.(예를 들면 세미콜론)
  • SQL statements other than SELECT are recognized automatically as non-composable. As a consequence, the full results of stored procedures are always returned to the client and any LINQ operators applied after FromSql are evaluated in-memory.(해석불가)

 Stored Procedure

Entity Framework는 SP(Stored Procedure)로 데이터베이스 테이블에 일정한 로직을 수행하도록 할 수 있다.Raw Queries는 SP를 실행하는 데 사용된다.

 

SP 예시

이는 실행되면 Customers의 모든 레코드를 리턴한다.

IF NOT EXISTS (SELECT * FROM sys.objects WHERE object_id =
	OBJECT_ID(N'[dbo].[GetAllCustomers]') AND type in(N'P',N'PC'))
    
BEGIN
	
    EXEC dbo.sp_execcutesql @statement = N'
    CREATE PROCEDURER [dbo].[GetAllCustomers]
    AS
    SELECT * FROM dbo.Customers
    '
END
GO

EFCore 에서 해당 SP를 FromSql() 메소드를 사용해 실행할 수 있다.

using (var context = new MyContext())
{
    var customers = context.Customers
        .FromSql("EXECUTE dbo.GetAllCustomers")
        .ToList();
}

또한 SP를 실행할 때 파라미터를 전달할 수도 있다.

IF NOT EXISTS (SELECT * FROM sys.objects WHERE object_id = 
   OBJECT_ID(N'[dbo].[GetCustomer]') AND type in (N'P', N'PC'))

BEGIN

   EXEC dbo.sp_executesql @statement = N'
   CREATE PROCEDURE [dbo].[GetCustomer]
   @CustomerID int
   AS
   SELECT * FROM dbo.Customers 
   WHERE CustomerID = @CustomerID
   '
END
GO

이는 Customers에서 파라미터로 전달된 CustomerID를 기반으로 특정한 레코드를 리턴한다.

using (var context = new MyContext())
{
    var customer = context.Customers
        .FromSql("EXECUTE dbo.GetCustomer 1")
        .ToList();
}

C# string interpolation을 써서 파라미터 값을 전달할 수도 있다.

using (var context = new MyContext())
{
    int customerId = 1;
    var customer = context.Customers
        .FromSql($"EXECUTE dbo.GetCustomer {customerId}")
        .ToList();
}

 

참고

- https://entityframeworkcore.com

 

Entity Framework Core

(Documentation made by ZZZ Projects & .NET Community) What's Entity Framework Core? Entity Framework Core is an ORM made by Microsoft. It allows performing CRUD operations without having to write SQL queries. It supports Code First, Database First, Stored

entityframeworkcore.com