contract@chaoreumsoft.co.kr |    031-921-0523

Entity Framework Core의 연결이 끊긴 시나리오에 데이터 삽입

페이지 정보

작성자최고관리자

본문

연결이 끊긴 시나리오에서 데이터를 저장하는 것은 연결된 시나리오와 약간 다릅니다.


연결이 끊긴 시나리오에서 DbContext는 엔터티가 현재 DbContext 인스턴스의 범위를 벗어나 추가되거나 수정되었기 때문에 연결이 끊긴 엔터티를 인식하지 못합니다.


따라서 데이터베이스에 대한 CUD(Create, Update, Delete) 작업을 수행하려면 연결이 끊긴 엔터티를 적절한 EntityState가 있는 컨텍스트에 연결해야 합니다.


다음 그림은 연결이 끊긴 시나리오의 CUD 작업을 보여줍니다.


1a2e7a9eaee8762bd09653a13bd6820c_1651472408_2298.png


위 그림과 같이 연결이 끊긴 엔터티(DbContext에서 추적하지 않는 엔터티)는 적절한 EntityState를 사용하여 DbContext에 연결해야 합니다.

예를 들어 새 엔터티에 대해 추가된 상태, 편집된 엔터티에 대해 수정된 상태 및 삭제된 엔터티에 대해 삭제됨 상태로, SaveChanges() 메서드가 호출될 때 데이터베이스에 INSERT, UPDATE 또는 DELETE 명령이 생성됩니다.


연결이 끊긴 시나리오에서 Entity Framework Core를 사용하여 DB 테이블에 레코드를 삽입, 업데이트 또는 삭제하려면 다음 단계를 수행해야 합니다.


1. 적절한 EntityState를 사용하여 DbContext에 엔터티를 연결합니다. 추가, 수정 또는 삭제됨

2. SaveChanges() 메서드 호출


다음 예는 위의 단계를 사용하여 데이터베이스에 새 레코드를 삽입하는 방법을 보여줍니다.


//Disconnected entity
var std = new Student(){ Name = "Bill" };

using (var context = new SchoolContext())
{
    //1. Attach an entity to context with Added EntityState
    context.Add<Student>(std);
    
    //or the followings are also valid
    // context.Students.Add(std);
    // context.Entry<Student>(std).State = EntityState.Added;
    // context.Attach<Student>(std);
                  
    //2. Calling SaveChanges to insert a new record into Students table
    context.SaveChanges();
}

위의 예에서 std는 Student 엔터티의 연결이 끊긴 인스턴스입니다. context.Add<Student>() 메서드는 학생 엔터티를 추가됨 상태의 컨텍스트에 연결합니다. SaveChanges() 메서드는 다음 INSERT 문을 빌드하고 실행합니다.


exec sp_executesql N'SET NOCOUNT ON;
INSERT INTO [Students] ([Name])
VALUES (@p0);
SELECT [StudentId]
FROM [Students]
WHERE @@ROWCOUNT = 1 AND [StudentId] = scope_identity();',N'@p0 nvarchar(4000), 
@p1 nvarchar(4000) ',@p0=N'Bill'
go

EF Core는 추가됨 상태로 엔터티를 추가하는 여러 방법을 제공합니다. 위의 예에서 context.Students.Add(std);, context.Entry<Student>(std).State = EntityState.Added; 및 context.Attach<Student>(std); 위와 동일한 INSERT 문이 생성됩니다.


Entity Framework Core는 연결이 끊긴 엔터티를 추가된 EntityState로 연결하는 다음 DbContext 및 DbSet 메서드를 제공하며, 이 메서드는 차례로 데이터베이스에서 INSERT 문을 실행합니다.


DbContext MethodsDbSet MethodsDescription
DbContext.AttachDbSet.Attach엔터티를 DbContext에 연결합니다. Key 속성에 값이 있는 엔터티에는 Unchanged 상태를 설정하고 Key 속성이 비어 있거나 데이터 유형의 기본값을 가진 엔터티에는 added 상태를 설정합니다.
DbContext.AddDbSet.Add상태가 추가된 DbContext에 엔터티를 연결합니다.
DbContext.AddRangeDbSet.AddRange상태가 추가된 DbContext에 엔터티 컬렉션을 연결합니다.
DbContext.Entry-변경 내용 추적 정보 및 작업에 대한 액세스를 제공하는 지정된 엔터티에 대한 EntityEntry를 가져옵니다.
DbContext.AddAsyncDbSet.AddAsync상태가 추가된 DbContext에 엔터티를 연결하고 그렇지 않은 경우 추적을 시작하는 비동기 메서드입니다. SaveChangesAsync()가 호출되면 데이터가 데이터베이스에 삽입됩니다.
DbContext.AddRangeAsyncDbSet.AddRangeAsync한 번에 상태가 추가된 DbContext에 여러 엔터티를 연결하고 그렇지 않은 경우 추적을 시작하는 비동기 메서드입니다. SaveChangesAsync()가 호출되면 데이터가 데이터베이스에 삽입됩니다.


참고: 위의 DbContext 메서드는 EF Core에 도입되었습니다(EF 6 이하에서는 사용할 수 없음). DbContext 및 DbSet 메서드는 모두 동일한 작업을 수행합니다. 어느 것을 사용할지는 코딩 패턴과 선호도에 따라 다릅니다.


단일 엔트리 업데이트


다음 예는 연결이 끊긴 엔터티를 업데이트하는 방법을 보여줍니다.

// Disconnected Student entity
var stud = new Student(){ StudentId = 1, Name = "Bill" };

stud.Name = "Steve"; 

using (var context = new SchoolContext())
{
    context.Update<Student>(stud);

    // or the followings are also valid
    // context.Students.Update(stud);
    // context.Attach<Student>(stud).State = EntityState.Modified;
    // context.Entry<Student>(stud).State = EntityState.Modified; 

    context.SaveChanges(); 
}

위의 예에서 stud는 유효한 Key 속성 값(StudentId = 1)을 가지고 있기 때문에 기존 Student 엔터티 개체라고 가정합니다.


Entity Framework Core는 지정된 엔터티를 컨텍스트에 연결하고 EntityState를 Modified로 설정하는 DbContext.Update() 메서드를 도입했습니다. 또는 DbSet.Update() 메서드(context.Students.Update(stud))를 사용하여 동일한 작업을 수행할 수도 있습니다.


위의 예는 데이터베이스에서 다음 UPDATE 문을 실행합니다.

exec sp_executesql N'SET NOCOUNT ON;
UPDATE [Students] SET [Name] = @p0
WHERE [StudentId] = @p1;
SELECT @@ROWCOUNT;
',N'@p1 int,@p0 nvarchar(4000)',@p1=1,@p0=N'Steve'
go


여러개 엔트리 업데이트

​DbContext.UpdateRange 또는 DbSet.UpdateRange 메서드를 사용하여 엔터티 컬렉션 또는 배열을 DbContext에 연결하고 해당 EntityState를 한 번에 수정됨으로 설정합니다.

var modifiedStudent1 = new Student()
{
    StudentId = 1,
    Name = "Bill"
};

var modifiedStudent2 = new Student()
{
    StudentId = 3,
    Name = "Steve"
};

var modifiedStudent3 = new Student()
{
    StudentId = 3,
    Name = "James"
};

IList<Student> modifiedStudents = new List<Student>()
{
    modifiedStudent1,
    modifiedStudent2,
    modifiedStudent3
};

using (var context = new SchoolContext())
{
    context.UpdateRange(modifiedStudents);
    
    // or the followings are also valid
    //context.UpdateRange(modifiedStudent1, modifiedStudent2, modifiedStudent3);
    //context.Students.UpdateRange(modifiedStudents);
    //context.Students.UpdateRange(modifiedStudent1, modifiedStudent2, modifiedStudent3);
                
    context.SaveChanges();
}

보시다시피 UpdateRange 메서드에는 두 개의 오버로드가 있습니다.


하나의 오버로드는 엔터티 컬렉션을 사용하고 두 번째 오버로드는 object[]를 매개변수로 사용합니다. DbSet.UpdateRange 메서드는 DbContext.UpdateRange 메서드와 동일한 방식으로 작동합니다.


EF Core는 위의 예에서 모든 엔터티에 대한 UPDATE 문을 빌드하여 성능을 개선하고 단일 데이터베이스 왕복으로 실행합니다.

exec sp_executesql N'SET NOCOUNT ON; 

UPDATE [Students] SET [Name] = @p0
WHERE [StudentId] = @p1;
SELECT @@ROWCOUNT;

UPDATE [Students] SET [Name] = @p2
WHERE [StudentId] = @p3;
SELECT @@ROWCOUNT;

UPDATE [Students] SET [Name] = @p4
WHERE [StudentId] = @p5;
SELECT @@ROWCOUNT;

',N'@p1 int,@p0 nvarchar(4000),@p3 int,@p2 nvarchar(4000),@p5 int,@p4 nvarchar(4000)',
@p1=1,@p0=N'Bill',@p3=2,@p2=N'Steve',@p5=3,@p4=N'James'
go

엔트리 상태 변경

Update 메서드는 키 속성 값을 기반으로 EntityState를 설정합니다. 루트 또는 자식 엔터티의 키 속성이 비어 있거나 지정된 데이터 형식의 null 또는 기본값인 경우 Update() 메서드는 이를 새 엔터티로 간주하고 해당 EntityState를 Entity Framework Core 2.x에서 추가됨으로 설정합니다.

public static void Main()
{
    var newStudent = new Student()
    {
        Name = "Bill"
    };

    var modifiedStudent = new Student()
    {
        StudentId = 1,
        Name = "Steve"
    };

    using (var context = new SchoolContext())
    {
        context.Update<Student>(newStudent);
        context.Update<Student>(modifiedStudent);

        DisplayStates(context.ChangeTracker.Entries());
    }
}
private static void DisplayStates(IEnumerable<EntityEntry> entries)
{
    foreach (var entry in entries)
    {
        Console.WriteLine($"Entity: {entry.Entity.GetType().Name},
                 State: {entry.State.ToString()} ");
    }
}
Output:

 

Entity: Student, State: Added
Entity: Student, State: Modified 


위의 예에서 newStudent에는 Key 속성 값(StudentId)이 없습니다. 따라서 Update() 메서드는 이를 추가됨으로 표시하지만 modifyStudent에는 값이 있으므로 수정됨으로 표시됩니다.


Exception:

​Update 및 UpdateRange 메서드는 DbContext의 인스턴스가 이미 동일한 키 속성 값을 가진 엔터티를 추적하고 있는 경우 InvalidOperationException을 throw합니다. 다음 예를 고려하십시오.

var student = new Student()
{
    StudentId = 1,
    Name = "Steve"
};

using (var context = new SchoolContext())
{
    // loads entity in a conext whose StudentId is 1
    context.Students.First<Student>(s => s.StudentId == 1); 

    // throws an exception as it already tracking entity with StudentId=1
    context.Update<Student>(student); 

    context.SaveChanges();
}

위의 예에서 컨텍스트 개체는 StudentId가 1인 Student 엔터티를 로드하고 추적을 시작합니다. 따라서 동일한 키 값으로 엔터티를 연결하면 다음 예외가 발생합니다.


The instance of entity type 'Student' cannot be tracked because another instance with the same key value for {'StudentId'} is already being tracked. When attaching existing entities, ensure that only one entity instance with a given key value is attached. Consider using 'DbContextOptionsBuilder.EnableSensitiveDataLogging' to see the conflicting key values. 



다음 예는 연결이 끊긴 시나리오에서 엔터티를 삭제하는 다양한 방법을 보여줍니다.

// entity to be deleted
var student = new Student() {
        StudentId = 1
};

using (var context = new SchoolContext()) 
{
    context.Remove<Student>(student);
   
    // or the followings are also valid
    // context.RemoveRange(student);
    //context.Students.Remove(student);
    //context.Students.RemoveRange(student);
    //context.Attach<Student>(student).State = EntityState.Deleted;
    //context.Entry<Student>(student).State = EntityState.Deleted;
    
    context.SaveChanges();
}

위의 예에서 유효한 StudentId가 있는 Studnet 엔터티는 Remove() 또는 RemoveRange() 메서드를 사용하여 컨텍스트에서 제거됩니다. 데이터는 SaveChanges()의 데이터베이스에서 삭제됩니다. 위의 예는 데이터베이스에서 다음 삭제 명령을 실행합니다.

exec sp_executesql N'SET NOCOUNT ON;
DELETE FROM [Students]
WHERE [StudentId] = @p0;
SELECT @@ROWCOUNT;
',N'@p0 int',@p0=1
go

참고: DbContext.Remove() 및 DbContext.RemoveRange() 메서드는 삭제 작업을 쉽게 하기 위해 EF Core에 새로 도입되었습니다.


Exception

Remove() 또는 RemoveRange() 메서드에서 지정된 엔터티의 키 값이 해당 데이터베이스 테이블에 없으면 EF Core에서 예외가 발생합니다. 다음 예제에서는 예외를 발생시킵니다.

var student = new Student() {
    StudentId = 50
};

using (var context = new SchoolContext()) {

    context.Remove<Student>(student);

    context.SaveChanges();
}

위의 예에서 StudentId = 50인 Student는 데이터베이스에 존재하지 않습니다. 따라서 EF Core는 다음을 던집니다.

DbUpdateConcurrencyException:

Database operation expected to affect 1 row(s) but actually affected 0 row(s). Data may have been modified or deleted since entities were loaded.

​따라서 위의 예외를 적절하게 처리하거나 id를 가진 해당 데이터가 삭제되기 전에 데이터베이스에 존재하는지 확인해야 합니다.
var student = new Student() {
    StudentId = 50
};

using (var context = new SchoolContext()) 
{
    try
    {
        context.Remove<Student>(deleteStudent);
        context.SaveChanges();
    }    
    catch (DbUpdateConcurrencyException ex)
    {
        throw new Exception("Record does not exist in the database");
    }
    catch (Exception ex)
    {
        throw;
    }
}


레코드 여러개 삭제

DbContext.RemoveRange() 또는 DbSet.RemoveRange() 메서드를 사용하여 여러 엔터티를 한 번에 제거할 수 있습니다.

IList<Student> students = new List<Student>() {
    new Student(){ StudentId = 1 },
    new Student(){ StudentId = 2 },
    new Student(){ StudentId = 3 },
    new Student(){ StudentId = 4 }
};

using (var context = new SchoolContext()) 
{
    context.RemoveRange(students);
    
    // or
    // context.Students.RemoveRange(students);
    
    context.SaveChanges();
}


위의 예는 단일 데이터베이스 방식에서 데이터베이스에서 4개의 레코드를 삭제합니다. 따라서 EF Core는 성능을 향상시켰습니다.


exec sp_executesql N'SET NOCOUNT ON;
DELETE FROM [Students]
WHERE [StudentId] = @p0;
SELECT @@ROWCOUNT;


DELETE FROM [Students]
WHERE [StudentId] = @p1;
SELECT @@ROWCOUNT;

DELETE FROM [Students]
WHERE [StudentId] = @p2;
SELECT @@ROWCOUNT;


DELETE FROM [Students]
WHERE [StudentId] = @p3;
SELECT @@ROWCOUNT;

',N'@p0 int,@p1 int',@p0=1,@p1=2,@p2=3,@p3=4
go

관련 데이터 삭제

엔터티가 일대일 또는 일대다와 같은 다른 엔터티와 관계가 있는 경우 루트 엔터티가 삭제될 때 관련 데이터를 삭제하는 것은 관계가 구성되는 방식에 따라 다릅니다.


예를 들어 Student 및 Grade 엔터티에 일대다 관계가 있다고 가정합니다. 

특정 GradeId에 대한 많은 학생 기록이 있습니다. 데이터베이스에서 관련 학생 기록이 있는 성적을 삭제하려고 하면 EF에서 참조 무결성 오류가 발생합니다. 이 문제를 해결하기 위해 Fluent API를 사용하여 참조 제약 조건 작업 옵션을 정의할 수 있습니다. 예를 들어 아래와 같이 관계에 대한 계단식 삭제 옵션을 구성할 수 있습니다.

modelBuilder.Entity<Student>()
    .HasOne<Grade>(s => s.Grade)
    .WithMany(g => g.Students)
    .HasForeignKey(s => s.GradeId)
    .OnDelete(DeleteBehavior.Cascade);

이제 Grade 엔터티를 삭제하면 관련된 모든 학생 레코드도 데이터베이스에서 삭제됩니다.

SetNull, ClientSetNull 및 Restrict와 같이 EF Core에서 사용할 수 있는 다른 참조 제약 조건 작업 옵션이 있습니다.




Tag
Entity Framework Core 사용법, EF Core
© Chaoreumsoft Corp. All rights reserved.