.NET/C#

[C#] LINQ(Language INtegrated Query) - 2. EF Core와 집계처리

클리엘 2022. 7. 5. 13:32
728x90

2. EF Core와 LINQ

 

'1. LINQ 표현식'에서는 LINQ를 통해 배열을 필터링하거나 정렬하는 예를 살펴보았습니다. 그리고 예제에서 사용된 소스는 List혹은 배열로서 단순한 형태의 데이터를 기반으로 하였습니다. 지금부터는 이 데이터 소스를 바꿔서 실제 DB의 Entity를 기반으로 다시 살펴보고자 합니다.

 

(1) 모델 설정

 

이를 위해 EF Core model을 정의해야 하는데 여기서는 MS SQL Server와 Northwind DB를 통해 Model을 생성할 것입니다.

 

자세한 내용은 아래 글을 참고하시기 바랍니다.

 

[C#] Entity Framework Core - 1. 시작/설정하기

 

[C#] Entity Framework Core - 1. 시작/설정하기

Entity Framework Core의 목적을 간단히 설명하면 개체 관계 매핑(ORM:Object-Relational Mapping) 기술로서 MS-SQL이나 SQLite와 같은 데이터베이스에 데이터를 읽고 쓰는 데 사용되는 기술이라고 할 수 있습니다.

lab.cliel.com

public class Northwind : DbContext
{
    public DbSet<Category>? Categories { get; set; }
    public DbSet<Product>? Products { get; set; }
    
    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        string connection = "Server=.;user id=sa;password=123!;Database=Northwind;MultipleActiveResultSets=true;";
        optionsBuilder.UseSqlServer(connection);
    }
}

public class Category
{
    public int CategoryId { get; set; }
    
    [Required]
    [StringLength(15)]
    public string CategoryName { get; set; } = null!;
    public string? Description { get; set; }
}

public class Product
{
    public int ProductId { get; set; }

    [Required]
    [StringLength(40)]
    public string ProductName { get; set; } = null!;
    public int? SupplierId { get; set; }
    public int? CategoryId { get; set; }

    [StringLength(20)]
    public string? QuantityPerUnit { get; set; }

    [Column(TypeName = "money")]
    public decimal? UnitPrice { get; set; }
    public short? UnitsInStock { get; set; }
    public short? UnitsOnOrder { get; set; }
    public short? ReorderLevel { get; set; }
    public bool Discontinued { get; set; }
}

(2) 필터링및 정렬

 

위 Modeling을 토대로 Project에 대한 필터링은 대략 다음과 같은 방법으로 수행할 수 있습니다.

using (Northwind db = new())
{
    IQueryable<Product>? ProductCategory2 = db.Products?.Where(x => x.CategoryId == 2);

    if (ProductCategory2 != null)
    {
        foreach (var item in ProductCategory2)
        {
            Console.WriteLine(item.ProductName);
        }
    }
}

db.Products는 DbSet<T>형식을 반환하는데 DbSet<T>는 IEnumerable<T>를 구현하고 있으므로 LINQ를 질의하는 데 사용할 수 있고 Entity의 컬렉션 데이터를 변경/삭제/추가하는 동작을 수행할 수 있습니다.

 

Where메서드를 통해 반환된 배열은 IEnumerable<T>가 아닌 IQueryble<T>의 형식입니다. 이는 메모리에서 식 트리를 통해 query를 빌드하는 LINQ provider를 사용한다는 것을 나타냅니다. LINQ provider는 트리 같은 데이터 구조에서 코드를 나타내며 동적 쿼리를 만들 수 있습니다.

 

LINQ 표현식은 다른 SQL과 같은 쿼리식으로 변환될 것이며 이후 foreach를 통해 열거를 시도하거나 ToArray()와 같은 메서드를 호출하면 쿼리를 실행하고 결과를 가져옵니다.

 

(3) 다른 타입으로 투영(Projection)하기

 

위의 Modeling부분에서 Product라는 클래스를 생성했는데 이는 new키워드를 통해 해당 클래스의 인스턴스를 생성할 수 있다는 것이고 인스턴스화를 통해 객체의 필드와 속성에 초기값을 설정할 수 있다는 것을 의미합니다.

Product p = new() { ProductName = "신제품" };

그런데 C#3.0 이후부터는 var키워드를 사용해 익명 타입의 인스턴스를 생성할 수 있으므로 아래와 같은 구현도 가능합니다.

var p = new { ProductName = "신제품" };

비록 타입을 명시하지 않았지만 컴파일러는 ProductName이라는 문자열 형식의 속성 하나를 갖는 객체로 타입을 유추하여 처리합니다. 이는 LINQ를 현재의 타입에서 명시적으로 타입을 지정하는 것 없이 다른 타입으로 투영하는 쿼리를 작성할 때 유용하게 사용될 수 있습니다. 이러한 방식은 지역변수에서 var키워드를 사용하는 것으로만 구현될 수 있습니다.

using (Northwind db = new())
{
    DbSet<Product>? products = db.Products;

    var newProducts = products?.Where(x => x.ProductId <= 10).Select(x => new { //익명타입 구현
        x.ProductId,
        x.ProductName,
        x.QuantityPerUnit
    });

    foreach(var item in newProducts) {
        Console.WriteLine(item.ProductName);
    }
}

(4) Join과 Grouping

 

● Join

 

Join은 4개의 매개변수를 필요로 합니다.

using (Northwind db = new())
{
    var result = db.Products?.Join(
        inner: db.Categories,
        outerKeySelector: p => p.CategoryId,
        innerKeySelector: c => c.CategoryId,
        resultSelector: (p, c) => new { c.CategoryName, p.ProductName }
    );

    foreach(var item in result) {
        Console.WriteLine($"그룹명 : {item.CategoryName}, 제품명 : {item.ProductName}");
    }
}

예제에서 inner는 Join 할 대상인 Categories를 지정하였으며 outerKeySelector는 Products에서 Join할 속성을, innerKeySelector에서는 Categories에서 Join할 속성을 지정하였습니다. 마지막으로 resultSelect에서는 Products와 Categoryies에서 Join후에 가져올 속성을 지정하고 이를 표시하도록 하고 있습니다.

 

● GroupJoin

 

GroupJoin은 Join과 달리 특정 값을 기준으로 일치하는 Collection을 IEnumerable<T> 형식으로 Group화 한다는 특징이 있습니다.

using (Northwind db = new())
{
    var result = db.Categories.AsEnumerable().GroupJoin(
        inner: db.Products,
        outerKeySelector: c => c.CategoryId,
        innerKeySelector: p => p.CategoryId,
        resultSelector: (c, p) => new { c.CategoryName, products = p }
    );

    foreach(var item in result) {
        Console.WriteLine($"그룹명 : {item.CategoryName}");

        foreach(var product in item.products) {
            Console.WriteLine($"해당 그룹의 제품 : {product.ProductName}");
        }
    }
}

예제에서는 Categories를 기준으로 Products에 대한 Group화를 시도하고 있습니다. resultSelector를 보면 CategoryID값을 기준으로 Products의 항목(p)을 IEnumerable<T>로 받고 있고 뒤이어 이를 products속성을 통해 열거하여 표시하도록 하고 있습니다.

 

또 하나 특이한 점은 Cateogories에서 결과를 받을 때 AsEnumerable()메서드를 사용하고 있다는 점입니다. 이는 모든 LINQ메서드가 식 트리를 SQL과 같은 쿼리 구문으로 변환하지 못하기 때문입니다. 따라서 위의 경우 우선적으로 IEnumerable<T>혹은 IQueryable<T>형태로 처리된 다음 다시 LINQ가 실행되도록 해야 합니다.

 

(5) 집계(Aggregating)

 

LINQ에는 Average나 Sum과 같은 몇몇 집계를 산출하기 위한 메서드가 존재합니다.

using (Northwind db = new())
{
    int cnt = db.Products.Count(); //제품 건수
    Console.WriteLine(cnt);

    decimal? price_sum = db.Products.Sum(x => x.UnitPrice); //UnitPrice 합계
    Console.WriteLine(price_sum);

    decimal? price_average = db.Products.Average(x => x.UnitPrice); //UnitPrice 평균
    Console.WriteLine(price_average);
}
728x90