연습 - 데이터와 상호 작용

완료됨

이전 연습에서는 엔터티 클래스와 데이터베이스 컨텍스트를 만들었습니다. 그런 다음, EF Core 마이그레이션을 사용하여 데이터베이스 스키마를 만들었습니다.

이 연습에서는 PizzaService 구현을 완료합니다. 서비스는 EF Core를 사용하여 데이터베이스에서 CRUD 작업을 수행합니다.

CRUD 작업 코딩

PizzaService 구현을 완료하려면 Services\PizzaService.cs에서 다음 단계를 완료합니다.

  1. 예제와 같이 다음과 같이 변경합니다.

    1. using ContosoPizza.Data; 지시문을 추가합니다.
    2. using Microsoft.EntityFrameworkCore; 지시문을 추가합니다.
    3. 생성자 앞에 PizzaContext에 대한 클래스 수준 필드를 추가합니다.
    4. PizzaContext 매개 변수를 허용하도록 생성자 메서드 시그니처를 변경합니다.
    5. 매개 변수를 필드에 할당하도록 생성자 메서드 코드를 변경합니다.
    using ContosoPizza.Models;
    using ContosoPizza.Data;
    using Microsoft.EntityFrameworkCore;
    
    namespace ContosoPizza.Services;
    
    public class PizzaService
    {
        private readonly PizzaContext _context;
    
        public PizzaService(PizzaContext context)
        {
            _context = context;
        }
    
        /// ...
        /// CRUD operations removed for brevity
        /// ...
    }
    

    이전에 Program.cs에 추가한 AddSqlite 메서드 호출은 종속성 주입을 위해 PizzaContext를 등록했습니다. PizzaService 인스턴스가 만들어지면 PizzaContext가 생성자로 주입됩니다.

  2. GetAll 메서드를 다음 코드로 바꿉니다.

    public IEnumerable<Pizza> GetAll()
    {
        return _context.Pizzas
            .AsNoTracking()
            .ToList();
    }
    

    위의 코드에서

    • Pizzas 컬렉션에는 피자 테이블의 모든 행이 포함됩니다.
    • AsNoTracking 확장 메서드는 EF Core에 변경 추적을 사용하지 않도록 지시합니다. 이 작업은 읽기 전용이므로 AsNoTracking에서 성능을 최적화할 수 있습니다.
    • 모든 피자는 ToList와 함께 반환됩니다.
  3. GetById 메서드를 다음 코드로 바꿉니다.

    public Pizza? GetById(int id)
    {
        return _context.Pizzas
            .Include(p => p.Toppings)
            .Include(p => p.Sauce)
            .AsNoTracking()
            .SingleOrDefault(p => p.Id == id);
    }
    

    위의 코드에서

    • Include 확장 메서드는 람다 식을 사용하여 ToppingsSauce 탐색 속성이 즉시 로드를 사용하여 결과에 포함하도록 지정합니다. 이 식이 없으면 EF Core가 해당 속성에 대해 null을 반환합니다.
    • SingleOrDefault 메서드는 람다 식과 일치하는 피자를 반환합니다.
      • 일치하는 레코드가 없으면 null이 반환됩니다.
      • 여러 레코드가 일치하면 예외가 throw됩니다.
      • 람다 식은 Id 속성이 id 매개 변수와 같은 레코드를 설명합니다.
  4. Create 메서드를 다음 코드로 바꿉니다.

    public Pizza Create(Pizza newPizza)
    {
        _context.Pizzas.Add(newPizza);
        _context.SaveChanges();
    
        return newPizza;
    }
    

    위의 코드에서

    • newPizza는 유효한 개체로 간주됩니다. EF Core는 데이터 유효성 검사를 수행하지 않으므로 ASP.NET Core 런타임 또는 사용자 코드에서 유효성 검사를 처리해야 합니다.
    • Add 메서드는 newPizza 엔터티를 EF Core의 개체 그래프에 추가합니다.
    • SaveChanges 메서드는 EF Core에 개체 변경 내용을 데이터베이스에 유지하도록 지시합니다.
  5. UpdateSauce 메서드를 다음 코드로 바꿉니다.

    public void UpdateSauce(int pizzaId, int sauceId)
    {
        var pizzaToUpdate = _context.Pizzas.Find(pizzaId);
        var sauceToUpdate = _context.Sauces.Find(sauceId);
    
        if (pizzaToUpdate is null || sauceToUpdate is null)
        {
            throw new InvalidOperationException("Pizza or sauce does not exist");
        }
    
        pizzaToUpdate.Sauce = sauceToUpdate;
    
        _context.SaveChanges();
    }
    

    위의 코드에서

    • 기존 PizzaSauce 개체에 대한 참조는 Find을 사용하여 만들어집니다. Find는 기본 키로 레코드를 쿼리하는 최적화된 메서드입니다. Find는 데이터베이스를 쿼리하기 전에 먼저 로컬 엔터티 그래프를 검색합니다.
    • Pizza.Sauce 속성은 Sauce 개체로 설정됩니다.
    • EF Core가 Pizza에 대한 Sauce 속성이 설정된 것을 검색하기 때문에 Update 메서드 호출은 필요하지 않습니다.
    • SaveChanges 메서드는 EF Core에 개체 변경 내용을 데이터베이스에 유지하도록 지시합니다.
  6. AddTopping 메서드를 다음 코드로 바꿉니다.

    public void AddTopping(int pizzaId, int toppingId)
    {
        var pizzaToUpdate = _context.Pizzas.Find(pizzaId);
        var toppingToAdd = _context.Toppings.Find(toppingId);
    
        if (pizzaToUpdate is null || toppingToAdd is null)
        {
            throw new InvalidOperationException("Pizza or topping does not exist");
        }
    
        if(pizzaToUpdate.Toppings is null)
        {
            pizzaToUpdate.Toppings = new List<Topping>();
        }
    
        pizzaToUpdate.Toppings.Add(toppingToAdd);
    
        _context.SaveChanges();
    }
    

    위의 코드에서

    • 기존 PizzaTopping 개체에 대한 참조는 Find을 사용하여 만들어집니다.
    • Topping 개체는 .Add 메서드를 사용하여 Pizza.Toppings 컬렉션에 추가됩니다. 컬렉션이 없으면 새로 만들어집니다.
    • SaveChanges 메서드는 EF Core에 개체 변경 내용을 데이터베이스에 유지하도록 지시합니다.
  7. DeleteById 메서드를 다음 코드로 바꿉니다.

    public void DeleteById(int id)
    {
        var pizzaToDelete = _context.Pizzas.Find(id);
        if (pizzaToDelete is not null)
        {
            _context.Pizzas.Remove(pizzaToDelete);
            _context.SaveChanges();
        }        
    }
    

    위의 코드에서

    • Find 메서드는 기본 키(이 경우 Id)로 피자를 검색합니다.
    • Remove 메서드는 EF Core의 개체 그래프에서 pizzaToDelete 엔터티를 제거합니다.
    • SaveChanges 메서드는 EF Core에 개체 변경 내용을 데이터베이스에 유지하도록 지시합니다.
  8. 모든 변경 내용을 저장하고 dotnet build를 실행합니다. 발생하는 모든 오류를 수정합니다.

데이터베이스 시드

PizzaService에 대한 CRUD 작업을 코딩했지만 데이터베이스에 양호한 데이터가 있는 경우 읽기 작업을 더 쉽게 테스트할 수 있습니다. 시작 시 데이터베이스를 시드하도록 앱을 수정해 보겠습니다.

경고

이 데이터베이스 시드 코드는 경합 조건을 고려하지 않으므로 변경 내용을 완화하지 않고 분산 환경에서 사용할 때는 주의해야 합니다.

  1. Data 폴더에 DbInitializer.cs라는 새 파일을 추가합니다.

  2. Data\DbInitializer.cs에 다음 코드를 추가합니다.

    using ContosoPizza.Models;
    
    namespace ContosoPizza.Data
    {
        public static class DbInitializer
        {
            public static void Initialize(PizzaContext context)
            {
    
                if (context.Pizzas.Any()
                    && context.Toppings.Any()
                    && context.Sauces.Any())
                {
                    return;   // DB has been seeded
                }
    
                var pepperoniTopping = new Topping { Name = "Pepperoni", Calories = 130 };
                var sausageTopping = new Topping { Name = "Sausage", Calories = 100 };
                var hamTopping = new Topping { Name = "Ham", Calories = 70 };
                var chickenTopping = new Topping { Name = "Chicken", Calories = 50 };
                var pineappleTopping = new Topping { Name = "Pineapple", Calories = 75 };
    
                var tomatoSauce = new Sauce { Name = "Tomato", IsVegan = true };
                var alfredoSauce = new Sauce { Name = "Alfredo", IsVegan = false };
    
                var pizzas = new Pizza[]
                {
                    new Pizza
                        { 
                            Name = "Meat Lovers", 
                            Sauce = tomatoSauce, 
                            Toppings = new List<Topping>
                                {
                                    pepperoniTopping, 
                                    sausageTopping, 
                                    hamTopping, 
                                    chickenTopping
                                }
                        },
                    new Pizza
                        { 
                            Name = "Hawaiian", 
                            Sauce = tomatoSauce, 
                            Toppings = new List<Topping>
                                {
                                    pineappleTopping, 
                                    hamTopping
                                }
                        },
                    new Pizza
                        { 
                            Name="Alfredo Chicken", 
                            Sauce = alfredoSauce, 
                            Toppings = new List<Topping>
                                {
                                    chickenTopping
                                }
                            }
                };
    
                context.Pizzas.AddRange(pizzas);
                context.SaveChanges();
            }
        }
    }
    

    위의 코드에서

    • DbInitializer 클래스와 Initialize 메서드는 모두 static으로 정의됩니다.
    • InitializePizzaContext 개체를 매개 변수로 허용합니다.
    • 세 테이블 모두 레코드가 없으면 Pizza, SauceTopping 개체가 만들어집니다.
    • Pizza 개체(및 해당 SauceTopping 탐색 속성)는 AddRange를 사용하여 개체 그래프에 추가됩니다.
    • 개체 그래프 변경 내용은 SaveChanges를 사용하여 데이터베이스에 커밋됩니다.

DbInitializer 클래스는 데이터베이스를 시드할 준비가 되었지만 Program.cs에서 호출해야 합니다. 다음 단계에서는 DbInitializer.Initialize를 호출하는 IHost에 대한 확장 메서드를 만듭니다.

  1. Data 폴더에 Extensions.cs라는 새 파일을 추가합니다.

  2. Data\Extensions.cs에 다음 코드를 추가합니다.

    namespace ContosoPizza.Data;
    
    public static class Extensions
    {
        public static void CreateDbIfNotExists(this IHost host)
        {
            {
                using (var scope = host.Services.CreateScope())
                {
                    var services = scope.ServiceProvider;
                    var context = services.GetRequiredService<PizzaContext>();
                    context.Database.EnsureCreated();
                    DbInitializer.Initialize(context);
                }
            }
        }
    }
    

    위의 코드에서

    • CreateDbIfNotExists 메서드는 IHost의 확장으로 정의됩니다.

    • PizzaContext 서비스에 대한 참조가 만들어집니다.

    • EnsureCreated는 데이터베이스가 존재하는지 확인합니다.

      중요

      EnsureCreated는 데이터베이스가 없을 경우 새로 만듭니다. 새 데이터베이스는 마이그레이션을 위해 구성되지 않았으므로 주의해서 사용해야 합니다.

    • DbIntializer.Initialize 메서드가 호출됩니다. PizzaContext 개체는 매개 변수로 전달됩니다.

  3. 마지막으로 Program.cs에서 // Add the CreateDbIfNotExists method call 주석을 다음 코드로 대체하여 새 확장 메서드를 호출합니다.

    app.CreateDbIfNotExists();
    

    이 코드는 앱이 실행 될 때마다 이전에 정의한 확장 메서드를 호출합니다.

  4. 모든 변경 내용을 저장하고 dotnet build를 실행합니다.

기본 CRUD 작업을 수행하고 시작할 때 데이터베이스를 시드하는 데 필요한 모든 코드를 작성했습니다. 다음 연습에서는 앱에서 이런 작업을 테스트합니다.

지식 점검

1.

읽기 전용 쿼리를 작성한다고 가정합니다. 개체 그래프 변경 내용 추적이 필요하지 않다고 EF Core에 어떻게 지정합니까?