[C#] Entity Framework Core - 4. 데이터 조작과 트랜잭션
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()을 수행하여 트랜잭션을 적용하도록 합니다.