6. EF Core를 통한 데이터 조작하기
EF Core를 사용해 데이터를 추가하거나 변경하거나 삭제하는 작업은 크게 어렵지 않습니다. DbContext는 자동적으로 변화에 대한 추적을 관리하므로 내부에서 반영된 여러 데이터의 추가/삭제/변경에 관한 사항을 로컬 Entity 통해 가지게 됩니다. 그리고 이 변경사항을 실제 데이터베이스에 반영하기를 시도(SaveChanges() 메서드를 통해)하면 Entity는 반영된 결과를 반환하게 될 것입니다.
(1) Insert
Insert는 해당 Entity에서 Add()메서드를 통해 실행할 수 있습니다.
using (Northwind db = new())
{
ILoggerFactory loggerFactory = db.GetService<ILoggerFactory>();
loggerFactory.AddProvider(new ConsoleLoggerProvider());
Product p = new()
{
ProductName = "신제품",
SupplierId = 12,
CategoryId = 1,
QuantityPerUnit = "1 box",
UnitPrice = 12.99m,
UnitsInStock = 10,
UnitsOnOrder = 0,
ReorderLevel = 30,
Discontinued = false
};
db.Products.Add(p);
int affected = db.SaveChanges();
if (affected == 1)
{
IQueryable<Product>? products = db.Products.OrderByDescending(x => x.ProductId);
foreach (var item in products) {
Console.WriteLine($"추가된 제품 : {item.ProductName}({item.ProductId})");
}
}
}
//추가된 제품 : 신제품(78)
//추가된 제품 : Original Frankfurter grune Soße(77)
(2) Update
Update는 Entity에서 변경하고자 하는 속성에 값을 지정함으로써 실행됩니다.
using (Northwind db = new())
{
ILoggerFactory loggerFactory = db.GetService<ILoggerFactory>();
loggerFactory.AddProvider(new ConsoleLoggerProvider());
Product? p = db.Products.Where(x => x.ProductId == 78).SingleOrDefault();
if (p is null) {
Console.WriteLine("해당 제품을 찾을 수 없습니다.");
return;
}
p.ProductName = "행사제품";
int affected = db.SaveChanges();
if (affected == 1)
{
IQueryable<Product>? products = db.Products.OrderByDescending(x => x.ProductId);
foreach (var item in products) {
Console.WriteLine($"추가된 제품 : {item.ProductName}({item.ProductId})");
}
}
}
//추가된 제품 : 행사제품(78)
//추가된 제품 : Original Frankfurter grune Soße(77)
(3) Delete
Delete는 Remove나 RemoveRange(다수의 데이터를 삭제)메서드를 사용합니다.
using (Northwind db = new())
{
ILoggerFactory loggerFactory = db.GetService<ILoggerFactory>();
loggerFactory.AddProvider(new ConsoleLoggerProvider());
Product? p = db.Products.Where(x => x.ProductId == 78).SingleOrDefault();
if (p is null) {
Console.WriteLine("해당 제품을 찾을 수 없습니다.");
return;
}
db.Products.Remove(p);
int affected = db.SaveChanges();
if (affected == 1)
{
IQueryable<Product>? products = db.Products.OrderByDescending(x => x.ProductId);
foreach (var item in products) {
Console.WriteLine($"추가된 제품 : {item.ProductName}({item.ProductId})");
}
}
}
//추가된 제품 : Original Frankfurter grune Soße(77)
//추가된 제품 : Lakkalikoori(76)
7. 트랜잭션(Transaction)
예제에서처럼 SaveChanges()메서드를 호출하면 암시적으로 트랜잭션이 시작되며 어떤 처리에서 오류가 발생하면 자동적으로 모든 변경사항이 롤백됩니다. 다수의 변경사항이 한꺼번에 적용되는 경우라 하더라도 오류가 발생하여 롤백되거나 정상 처리되면 모든 변경사항이 그대로 커밋될 수 있습니다.
트랜잭션은 변경사항이 순서대로 처리되는 동안 해당 테이블을 읽거나 쓰는것을 막기 위해 Lock 걸어둠으로써 데이터베이스의 무결성을 보증하도록 합니다. 트랜잭션의 특징을 ACID라고도 표현하는데 이는 아래의 특징을 일컫는 말입니다.
- A(atomic) : 트랜잭션이 모두 반영되거나 어떤것도 반영되지 않음
- C(consistent) : 트랜잭션 전/후의 데이터베이스 상태는 일관적임, 다만 이것은 코드의 로직에 따라 달라질 수 있는데 예를 들어 은행계좌 간 돈을 송금할 때 하나의 계좌에서 돈을 인출한 경우 다른 계좌에 돈을 입금하는 로직은 직접 구현
- I(Isolated) : 격리성을 의미하며 트랜잭션중에 변경사항은 다른 process에서는 숨겨짐. 선택 가능한 여러 격리 단계가 있으며 가장 강력한 격리 단계는 데이터의 무결성을 그만큼 보장할 수 있지만 많은 락이 적용되어야 하며 이는 다른 process에서 부정적인 영향을 줄 수 있음. Snapshot은 특별한 경우인데 다수의 Row에 대한 복사본을 생성함으로써락을 피하게 되지만 트랜잭션이 발생하는 동안 데이터베이스의 크기를 증가시킬 수 있음
- D(durable) : 트랜잭션이 처리되는 동안 실패가 발생하면 데이터는 다시 복구될 수 있음. 이는 2개의 Commit문구와 트랜잭션 Log를 통해 가능한 것임. 일단 트랜잭션이 커밋되고 나면 다음 처리에서 오류가 발생하는 경우라 하더라도 데이터에 대한 지속성을 보증할 수 있음
(1) 고립화 수준을 통한 트랜잭션 제어
아래는 설정 가능한 고립화 수준을 나열한 것입니다.
단계 | 락 | 특징 |
ReadUncommitted | 없음 | 트랜잭션에서 처리 중인(아직 커밋되지 않은) 데이터를 다른 트랜잭션이 읽는 것을 허용 - Dirty Read, Non-Repeatable Read, Phantom Read |
ReadCommitted | 트랜잭션이 시작되어 데이터의 처리가 시작되면 트랜잭션 종료시까지 읽기에 대한 락을 생성 | Dirty Read 방지하여 트랜잭션이 커밋되어 확정된 데이터만 읽도록 하는 것으로서 가장 일반적인 단계입니다. - Non-Repeatable Read, Phantom Read |
RepeatableRead | 데이터에 대한 읽기가 진행중일때 트랜잭션 종료시까지 데이터가 조작되는 것에 대한 락을 생성 | 이전 트랜잭션이 읽은 데이터는 트랜잭션이 종료될 때가지 다음 트랜잭션이 데이터를 변경하는 것을 방지하여 같은 데이터를 두 번 쿼리했을 때 일관성 있는 결과를 반환하도록 합니다. - Phantom data |
Serializable | key-range락을 적용하여 Insert나 Delete를 포함해 결과에 영향을 줄 수 있는 모든 Action을 차단 | 이전 트랜잭션이 읽은 데이터를 다음 트랜잭션이 변경하거나 삭제하지 못하고 중간에 새로운 데이터를 추가하는 것도 불가능합니다. 이는 완벽한 읽기 일관성 모드를 제공할 수 있습니다. |
Snapshot | 없음 | 없음 |
- Dirty Read : 아직 커밋되지 않은 수정 중인 데이터를 다른 트랜잭션에서 읽을 때 발생
- Non-Repeatable Read : 한 트랜잭션 내에서 같은 쿼리를 두 번 수행할 때, 그 사이에 다른 트랜잭션이 값을 수정 또는 삭제함으로써 두 쿼리의 결과가 다르게 나오는 현상(비 일관성)
- Phantom Read : 한 트랜잭션 안에서 데이터를 2회 이상 쿼리 하는 경우 처음 쿼리에 없던 데이터가 두 번째 쿼리에서 나타나는 현상
(2) 명시적 트랜잭션
Context의 데이터베이스 속성을 통해 트랜잭션을 명시적으로 제어할 수 있습니다. 이를 위해 우선 아래와 같이 Microsoft.EntityFrameworkCore.Storage Namespace를 Import 합니다. 이 Namespace는 IDbContextTransaction을 사용하기 위한 것입니다.
using Microsoft.EntityFrameworkCore.Storage;
그런 다음 Northwind 인스턴스를 생성하는 using안으로 트랜잭션을 명시적으로 시작하는 using구문을 추가합니다.
using (Northwind db = new())
{
using (IDbContextTransaction t = db.Database.BeginTransaction())
{
Console.WriteLine($"격리수준 : {t.GetDbTransaction().IsolationLevel}");
Product? p = db.Products.Where(x => x.ProductId == 78).SingleOrDefault();
if (p is null) {
Console.WriteLine("해당 제품을 찾을 수 없습니다.");
return;
}
db.Products.Remove(p);
int affected = db.SaveChanges();
t.Commit();
if (affected == 1)
{
IQueryable<Product>? products = db.Products.OrderByDescending(x => x.ProductId);
foreach (var item in products) {
Console.WriteLine($"{item.ProductName}({item.ProductId})");
}
}
}
}
//격리수준 : ReadCommitted
//Original Frankfurter grune Soße(77)
트랜잭션 using안에서는 현재 트랜잭션의 격리 수준을 표시한 뒤 이전 예제와 동일한 제품 삭제처리 로직을 포함시킵니다. 이때 SaveChanges() 메서드가 실행된 후 Commit()을 수행하여 트랜잭션을 적용하도록 합니다.
'.NET > C#' 카테고리의 다른 글
[C#] C#과 .NET6 시작하기 - 1. 개발환경설정 (0) | 2022.06.24 |
---|---|
[C#] Entity Framework Core - 5. Code First Model (0) | 2022.06.24 |
[C#] Entity Framework Core - 3. 질의하기및 Pattern 로드 (0) | 2022.06.24 |
[C#] Entity Framework Core - 2. 모델링(Modeling) (0) | 2022.06.24 |
[C#] Entity Framework Core - 1. 시작/설정하기 (0) | 2022.06.24 |