DbContext와 모델 매핑
형식을 표에 맞춥니다.
규약이 맞지 않을 때
3.1에서 클래스만 적었는데 표가 만들어졌습니다. 규약이 정해져 있기 때문입니다. 그런데 이미 있는 데이터베이스에 맞춰야 하는 경우가 많습니다. 표 이름이 TODO_LIST 이고 컬럼이 NUM·SUBJECT 라면 규약과 어긋납니다.
적는 방법이 둘입니다.
- 어트리뷰트 — 형식과 속성 위에 붙입니다. 2.3의 검사 규칙과 같은 모양입니다.
- Fluent API —
OnModelCreating에 코드로 적습니다.
둘 다 사용할 수 있고, 같은 것을 두 곳에 적으면 Fluent API 가 이깁니다. 아래에서 확인합니다.
같은 표를 세 가지로
먼저 규약만 따랐을 때입니다. 3.1과 같습니다.
public class Plain { public int Id { get; set; } public string Title { get; set; } = ""; public string? Memo { get; set; } public decimal Cost { get; set; } }
이 코드는 EF Core 와 데이터베이스가 있어야 하므로 브라우저에서 실행할 수 없습니다.
decimal 이 TEXT 가 된 것을
보십시오. SQLite 에 decimal 형식이 없어 글자로 담습니다.
같은 코드라도 데이터베이스가 다르면 다른 표가 만들어집니다.
SQL Server 였다면 decimal(18,2) 입니다.
어트리뷰트로 적으면
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; [Table("TODO_LIST")] public class Attr { [Key] [Column("NUM")] public int Num { get; set; } [Required] [MaxLength(20)] [Column("SUBJECT")] public string? Title { get; set; } [NotMapped] public string 임시 { get; set; } = ""; }
[NotMapped] 를 붙인
임시 는 컬럼이 되지 않았습니다. 화면에만
필요하고 담을 것은 아닌 값에 사용합니다.
Fluent API 로 적으면
protected override void OnModelCreating(ModelBuilder b) { b.Entity<Fluent>(e => { e.ToTable("TODO_LIST"); e.HasKey(x => x.Num); e.Property(x => x.Num).HasColumnName("NUM"); e.Property(x => x.Title).HasColumnName("SUBJECT").HasMaxLength(20).IsRequired(); e.Property(x => x.Done).HasDefaultValue(false); e.HasIndex(x => x.Title).IsUnique(); e.Ignore(x => x.임시); }); }
어트리뷰트로는 적을 수 없던 것이 둘 있습니다. 기본값과 인덱스입니다. 그래서 표를 제대로 맞추려면 Fluent API 가 필요한 자리가 생깁니다.
어느 쪽으로 적을 것인가
| 어트리뷰트 | Fluent API | |
|---|---|---|
| 적는 곳 | 형식 파일 안 | DbContext 한 곳 |
| 읽기 | 형식만 보면 보입니다 | 매핑이 한곳에 모입니다 |
| 할 수 있는 것 | 일부입니다 | 전부입니다 |
| 형식이 EF 를 아는가 | 압니다 — 참조가 붙습니다 | 모릅니다 |
마지막 줄이 정하는 자리입니다. 형식을 다른 계층에서도 사용한다면, 그 형식이 데이터베이스를 아는 것이 달갑지 않을 수 있습니다. 그때는 Fluent API 로 DbContext 쪽에 몰아 둡니다. 형식이 그 데이터베이스 전용이라면 어트리뷰트가 읽기 쉽습니다.
둘 다 적으면 Fluent API 가 이깁니다
[Table("어트리뷰트가_정한_이름")] public class Both { public int Id { get; set; } public string? Title { get; set; } } protected override void OnModelCreating(ModelBuilder b) => b.Entity<Both>().ToTable("Fluent가_정한_이름");
규약보다 어트리뷰트가, 어트리뷰트보다 Fluent API 가 앞섭니다.
어트리뷰트를 고쳤는데 바뀌지 않는다면
OnModelCreating 에 같은 것이 적혀 있는지 먼저
보십시오. 오류가 나지 않아 알아채기 어렵습니다.
DbContext 는 요청 하나에 하나입니다
3.1에서는 new TodoContext(…) 로 직접 만들었습니다.
웹에서는 그렇게 하지 않고 등록해 두고 받아 사용합니다.
// Program.cs — Build() 앞 builder.Services.AddDbContext<TodoContext>(o => o.UseSqlite(builder.Configuration.GetConnectionString("Default"))); // 컨트롤러 — 기본 생성자로 받습니다 public class TodosController(TodoContext db) : ControllerBase { [HttpGet] public IEnumerable<Todo> List() => db.Todos.AsNoTracking().ToList(); }
AddDbContext 는 Scoped 로 등록합니다.
요청 하나가 하나의 범위이므로, 같은 요청 안에서는 어디서 받든 같은
것이고 다른 요청과는 다릅니다.
services.AddDbContext<TodoContext>(…); var a = scope1.ServiceProvider.GetRequiredService<TodoContext>(); var b = scope1.ServiceProvider.GetRequiredService<TodoContext>(); var c = scope2.ServiceProvider.GetRequiredService<TodoContext>();
이 수명에는 까닭이 있습니다. 3.1에서 본 대로 DbContext 는 가져온 것을 지켜봅니다. 오래 살아 있으면 그 기억이 계속 쌓이고, 여러 요청이 하나를 함께 사용하면 동시에 건드려 깨집니다. DbContext 는 여러 스레드가 함께 사용하도록 만들어져 있지 않습니다.
그래서 싱글턴으로 등록하지 마십시오. 백그라운드 작업처럼 요청
밖에서 필요하다면 IDbContextFactory 를 등록해 그때마다
새로 만들어 사용합니다.
연결 문자열은 코드에 두지 않습니다
위에서 GetConnectionString("Default") 로 읽었습니다.
1.2에서 본 appsettings.json 의
ConnectionStrings 에서 가져옵니다.
비밀번호가 들어가는 자리라 저장소에 담지 않습니다. 개발
기계에서는 사용자 비밀(dotnet user-secrets)에,
운영에서는 환경 변수에 둡니다. 4.4와 5.1에서 다시 봅니다.
직접 해보기
아래 표에 맞는 클래스와 매핑을 적어보세요. 어느 방식이든 좋습니다.
CREATE TABLE MEMBER (
MEM_NO INT PRIMARY KEY,
MEM_NAME NVARCHAR(50) NOT NULL,
MEM_MEMO NVARCHAR(200) NULL
);
FullName 이라는 속성을 더하되
표에는 만들지 않고 이름과 성을 이어 돌려주게 하려면 어떻게 할지
적어보세요.
DbContext 를 AddSingleton 으로 등록하면 어떤 일이
생길지 두 가지 적어보세요.
- 규약이 맞지 않으면 어트리뷰트나 Fluent API 로 적습니다.
- 기본값과 인덱스는 Fluent API 로만 적을 수 있습니다.
- 같은 것을 두 곳에 적으면 Fluent API 가 이깁니다. 오류가 나지 않습니다.
- 같은 코드라도 데이터베이스가 다르면 다른 표가 만들어집니다(
decimal이 SQLite 에서 TEXT). AddDbContext는 Scoped 입니다. 싱글턴으로 두지 마십시오.