티스토리 뷰

저장 프로시저 결과 매핑 전략

키 없는 엔티티 타입 (Keyless Entity Type)으로 매핑 (권장):

  • 저장 프로시저의 결과가 BookTag 엔티티의 컬럼과 완전히 일치하지 않거나, 더 많은 조인된 정보(예: Book.Title, Tag.Name)를 포함하고 싶을 때 사용합니다.
  • 이 방법은 저장 프로시저의 결과를 읽기 전용으로 사용하며, 복잡한 쿼리나 보고서 출력에 매우 유용합니다.

여기서는 키 없는 엔티티 타입을 사용하여 저장 프로시저 결과를 매핑하는 방법을 예제로 설명하겠습니다.


예제: 저장 프로시저 결과 매핑

시나리오: BookId, TagId뿐만 아니라 Book의 제목(Title), Tag의 이름(Name)까지 포함된 상세 정보를 가져오는 저장 프로시저를 만들고 싶습니다.

1단계: MSSQL 저장 프로시저 생성

데이터베이스에 다음 저장 프로시저를 생성합니다.

SQL
 
-- MSSQL Server에서 실행할 저장 프로시저
CREATE PROCEDURE GetBookTagDetails
AS
BEGIN
    SELECT
        bt.BookId,
        bt.TagId,
        b.Title AS BookTitle, -- Book 테이블에서 제목 가져오기
        t.Name AS TagName,   -- Tag 테이블에서 이름 가져오기
        bt.CreatedDate       -- BookTag 테이블의 CreatedDate
    FROM
        BookTags bt
    JOIN
        Books b ON bt.BookId = b.Id
    JOIN
        Tags t ON bt.TagId = t.Id;
END;
GO

 

2단계: 키 없는 엔티티 타입 모델 정의

저장 프로시저의 결과 컬럼과 정확히 일치하는 새 C# 클래스를 정의합니다. 이 클래스는 데이터베이스 테이블에 직접 매핑되지 않으므로, 기본 키가 필요 없습니다.

// Models/BookTagDetailViewModel.cs
// 이 클래스는 데이터베이스 테이블에 직접 매핑되지 않고, 저장 프로시저의 결과를 받기 위한 DTO (Data Transfer Object) 역할을 합니다.
using System;

namespace CoreMVC.Models
{
    public class BookTagDetailViewModel
    {
        // 저장 프로시저의 SELECT 절 컬럼 이름과 정확히 일치해야 합니다.
        public int BookId { get; set; }
        public int TagId { get; set; }
        public string BookTitle { get; set; } // 조인된 Book의 제목
        public string TagName { get; set; }   // 조인된 Tag의 이름
        public DateTime CreatedDate { get; set; }
    }
}
 
 

3단계: AppDbContext에 키 없는 엔티티 타입 등록

AppDbContext에 새 DbSet을 추가하고, OnModelCreating에서 이 타입을 **키 없는 엔티티(Keyless Entity)**로 구성합니다.

// Data/AppDbContext.cs
using Microsoft.EntityFrameworkCore;
using CoreMVC.Models;

namespace CoreMVC.Data
{
    public class AppDbContext : DbContext
    {
        public AppDbContext(DbContextOptions<AppDbContext> options)
            : base(options)
        {
        }
        
        // ----- 새로운 DbSet 추가 (키 없는 엔티티) -----
        public DbSet<BookTagDetailViewModel> BookTagDetails { get; set; }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            base.OnModelCreating(modelBuilder);

            // --- 키 없는 엔티티 타입 구성 ---
            modelBuilder.Entity<BookTagDetailViewModel>()
                .HasNoKey(); // 이 엔티티는 기본 키가 없음을 EF Core에 알립니다.
            // 필요하다면 특정 뷰에 매핑할 수도 있습니다.
            // 저장 프로시저 결과를 직접 매핑할 때는 ToView()를 사용하지 않습니다. .ToView("SomeDatabaseView"); 
            // 실제 DB 뷰가 있다면 여기에 매핑
        }
    }
}


#### 4단계: 컨트롤러에서 저장 프로시저 호출 및 결과 사용

이제 컨트롤러에서 `FromSqlRaw` 또는 `FromSqlInterpolated` 메서드를 사용하여 
저장 프로시저를 실행하고 결과를 `BookTagDetailViewModel` 타입으로 받을 수 있습니다.


// Controllers/ProductsController.cs
using Microsoft.AspNetCore.Mvc;
using MyWebApp.Models;
using MyWebApp.Data;
using Microsoft.EntityFrameworkCore; // FromSqlRaw/FromSqlInterpolated를 위해 필요

namespace MyWebApp.Controllers
{
    public class ProductsController : Controller
    {
        private readonly AppDbContext _context; // AppDbContext로 변경

        public ProductsController(AppDbContext context) // AppDbContext로 변경
        {
            _context = context;
        }

        // 기존 Index 액션은 그대로
        public async Task<IActionResult> Index()
        {
            // 예시로 Product 목록을 그대로 보여줍니다.
            return View(await _context.Products.ToListAsync());
        }

        // --- 저장 프로시저 결과를 표시하는 새로운 액션 ---
        public async Task<IActionResult> BookTagDetails()
        {
            // 저장 프로시저 GetBookTagDetails를 호출하고 결과를 BookTagDetailViewModel에 매핑
            var bookTagDetails = await _context.BookTagDetails
                                             .FromSqlRaw("EXEC GetBookTagDetails")
                                             .ToListAsync();

            return View(bookTagDetails);
        }

        // Create, Edit, Delete 액션 등은 기존과 동일하게 Book/Tag/BookTag DbSet을 직접 사용합니다.
        // 예를 들어, BookTag에 새 데이터를 추가하는 Create 액션:
        [HttpGet]
        public IActionResult CreateBookTag()
        {
            ViewBag.Books = _context.Books.Select(b => new { b.Id, b.Title }).ToList();
            ViewBag.Tags = _context.Tags.Select(t => new { t.Id, t.Name }).ToList();
            return View(); // 빈 폼
        }

        [HttpPost]
        [ValidateAntiForgeryToken]
        public async Task<IActionResult> CreateBookTag(int bookId, int tagId)
        {
            var newBookTag = new BookTag { BookId = bookId, TagId = tagId, CreatedDate = DateTime.Now };
            _context.BookTags.Add(newBookTag); // BookTag DbSet을 직접 사용하여 INSERT
            await _context.SaveChangesAsync();
            TempData["SuccessMessage"] = "BookTag가 성공적으로 추가되었습니다!";
            return RedirectToAction(nameof(BookTagDetails));
        }

        // ... 기존 CRUD 액션 (생략) ...
    }
}
 
 
 

5단계: 저장 프로시저 결과 표시를 위한 뷰 (Views/Products/BookTagDetails.cshtml)

@model IEnumerable<CoreMVC.Models.BookTagDetailViewModel>

@{
    ViewData["Title"] = "도서-태그 상세 정보";
}

<h1>@ViewData["Title"]</h1>

<table class="table">
    <thead>
        <tr>
            <th>도서 ID</th>
            <th>태그 ID</th>
            <th>도서 제목</th>
            <th>태그 이름</th>
            <th>추가된 날짜</th>
        </tr>
    </thead>
    <tbody>
        @foreach (var item in Model)
        {
            <tr>
                <td>@Html.DisplayFor(modelItem => item.BookId)</td>
                <td>@Html.DisplayFor(modelItem => item.TagId)</td>
                <td>@Html.DisplayFor(modelItem => item.BookTitle)</td>
                <td>@Html.DisplayFor(modelItem => item.TagName)</td>
                <td>@Html.DisplayFor(modelItem => item.CreatedDate)</td>
            </tr>
        }
    </tbody>
</table>

<div>
    <a asp-action="Index">제품 목록으로 돌아가기</a>
</div>
 

정리:

  • DbSet<BookTag> 자체는 여전히 BookTags 테이블에 직접 매핑됩니다.
  • 저장 프로시저를 통해 Book, Tag 테이블을 조인한 결과를 DbSet<BookTag>에 "테이블처럼" 직접 매핑하는 것은 EF Core의 기본 설계에는 맞지 않습니다.
  • 저장 프로시저의 쿼리 결과(읽기 전용)를 가져오려면, FromSqlRaw 또는 FromSqlInterpolated 메서드를 사용하고, 그 결과를 받기 위한 별도의 키 없는 엔티티 타입(예: BookTagDetailViewModel)을 정의하는 것이 가장 일반적이고 권장되는 방법입니다. 이 방법은 저장 프로시저가 반환하는 컬럼에 정확히 일치하는 C# 객체를 얻을 수 있게 해줍니다.
728x90
반응형