다음을 통해 공유


ExecuteUpdate 및 ExecuteDelete

참고 항목

이 기능은 EF Core 7.0에서 도입되었습니다.

ExecuteUpdateExecuteDelete은(는) EF의 기존 변경 내용 추적 및 SaveChanges() 메서드를 사용하지 않고 데이터베이스에 데이터를 저장하는 방법입니다. 이 2가지 기술을 비교하며 소개하는 내용을 보려면 데이터 저장에서 개요 페이지를 참조하세요.

ExecuteDelete

특정 임계값 미만의 등급으로 모든 블로그를 삭제해야 한다고 가정해 보겠습니다. 기존의 SaveChanges() 접근 방식을 사용하려면 다음을 수행해야 합니다.

foreach (var blog in context.Blogs.Where(b => b.Rating < 3))
{
    context.Blogs.Remove(blog);
}

context.SaveChanges();

이 작업을 수행하는 데 매우 비효율적인 방법입니다. 필터와 일치하는 모든 블로그에 대해 데이터베이스를 쿼리한 다음, 모든 인스턴스를 쿼리, 구체화 및 추적합니다. 일치하는 엔터티의 수는 매우 많을 수 있습니다. 그런 다음, EF의 변경 추적기에서 각 블로그를 제거해야 한다고 알리고 SaveChanges()을(를) 호출하여 이러한 변경 내용을 적용합니다. 그러면 각 블로그에 대한 DELETE 명령문이 생성됩니다.

다음은 ExecuteDelete API를 통해 수행되는 동일한 작업입니다.

context.Blogs.Where(b => b.Rating < 3).ExecuteDelete();

이렇게 하면 익숙한 LINQ 연산자를 사용하여 블로그를 쿼리하는 것처럼 영향을 줄 블로그를 결정한 다음 EF에 데이터베이스에 대해 SQL DELETE을(를) 실행하도록 지시합니다.

DELETE FROM [b]
FROM [Blogs] AS [b]
WHERE [b].[Rating] < 3

보다 단순하고 짧을 뿐만 아니라 데이터베이스에서 데이터를 로드하거나 EF의 변경 추적기를 포함하지 않고 데이터베이스에서 매우 효율적으로 실행됩니다. 임의 LINQ 연산자를 사용하여 삭제할 블로그를 선택할 수 있습니다. 이는 해당 블로그를 쿼리하는 것처럼 데이터베이스에서 실행하기 위해 SQL로 변환됩니다.

ExecuteUpdate

이러한 블로그를 삭제하는 대신 숨기도록 속성을 변경하려면 어떻게 해야 하나요? ExecuteUpdate은(는) SQL UPDATE 명령문을 표현하는 유사한 방법을 제공합니다.

context.Blogs
    .Where(b => b.Rating < 3)
    .ExecuteUpdate(setters => setters.SetProperty(b => b.IsVisible, false));

ExecuteDelete와(과) 마찬가지로 먼저 LINQ를 사용하여 영향을 줄 블로그를 결정합니다. 그러나 ExecuteUpdate와(과) 함께 일치하는 블로그에 적용할 변경 내용도 표현해야 합니다. 이 작업은 ExecuteUpdate 호출 내에서 SetProperty을(를) 호출하고 변경할 속성(IsVisible) 및 새 값(false)이라는 두 인수를 제공하여 수행됩니다. 이렇게 하면 다음 SQL이 실행됩니다.

UPDATE [b]
SET [b].[IsVisible] = CAST(0 AS bit)
FROM [Blogs] AS [b]
WHERE [b].[Rating] < 3

여러 속성을 업데이트합니다.

ExecuteUpdate을(를) 사용하면 단일 호출에서 여러 속성을 업데이트할 수 있습니다. 예를 들어 IsVisible을(를) false로 설정하고 Rating을(를) 0으로 설정하려면 간단히 추가 SetProperty 호출을 함께 연결하기만 하면 됩니다.

context.Blogs
    .Where(b => b.Rating < 3)
    .ExecuteUpdate(setters => setters
        .SetProperty(b => b.IsVisible, false)
        .SetProperty(b => b.Rating, 0));

이렇게 하면 다음 SQL이 실행됩니다.

UPDATE [b]
SET [b].[Rating] = 0,
    [b].[IsVisible] = CAST(0 AS bit)
FROM [Blogs] AS [b]
WHERE [b].[Rating] < 3

기존 속성 값에 값을 참조합니다.

위의 예제에서는 속성을 새 상수 값으로 업데이트했습니다. ExecuteUpdate을(를) 사용하면 새 값을 계산할 때 기존 속성 값을 참조할 수도 있습니다. 예를 들어 일치하는 모든 블로그의 등급을 하나씩 높이려면 다음을 사용합니다.

context.Blogs
    .Where(b => b.Rating < 3)
    .ExecuteUpdate(setters => setters.SetProperty(b => b.Rating, b => b.Rating + 1));

두 번째 인수 SetProperty은(는) 이제 이전과 같은 상수가 아니라 람다 함수입니다. 해당 매개 변수는 b 업데이트 중인 블로그를 나타내며, b.Rating 해당 람다 내에서 변경이 발생하기 전에 등급을 포함합니다. 이렇게 하면 다음 SQL이 실행됩니다.

UPDATE [b]
SET [b].[Rating] = [b].[Rating] + 1
FROM [Blogs] AS [b]
WHERE [b].[Rating] < 3

ExecuteUpdate은(는) 현재 SetProperty 람다 내에서 탐색 참조를 지원하지 않습니다. 예를 들어 각 블로그의 새 등급이 모든 게시물 등급의 평균이 되도록 모든 블로그의 등급을 업데이트한다고 가정해 보겠습니다. 다음과 같이 ExecuteUpdate을(를) 사용하려고 할 수 있습니다.

context.Blogs.ExecuteUpdate(
    setters => setters.SetProperty(b => b.Rating, b => b.Posts.Average(p => p.Rating)));

그러나 EF는 먼저 Select을(를) 사용하여 평균 등급을 계산하고 무명 형식으로 프로젝션한 다음, 여기에 ExecuteUpdate을(를) 사용하여 이 작업을 수행할 수 있습니다.

context.Blogs
    .Select(b => new { Blog = b, NewRating = b.Posts.Average(p => p.Rating) })
    .ExecuteUpdate(setters => setters.SetProperty(b => b.Blog.Rating, b => b.NewRating));

이렇게 하면 다음 SQL이 실행됩니다.

UPDATE [b]
SET [b].[Rating] = CAST((
    SELECT AVG(CAST([p].[Rating] AS float))
    FROM [Post] AS [p]
    WHERE [b].[Id] = [p].[BlogId]) AS int)
FROM [Blogs] AS [b]

Change tracking

SaveChanges에 익숙한 사용자는 여러 변경 내용을 수행한 다음 SaveChanges을(를) 호출하여 이러한 모든 변경 내용을 데이터베이스에 적용하는 데 사용됩니다. 이러한 변경 내용을 누적하거나 추적하는 EF의 변경 추적기에서 이 작업을 수행할 수 있습니다.

ExecuteUpdateExecuteDelete은(는) 매우 다르게 작동합니다. 호출되는 시점에 즉시 적용됩니다. 즉, 단일 ExecuteUpdate 또는 ExecuteDelete 작업이 많은 행에 영향을 줄 수 있지만 이러한 여러 작업을 누적하고 예를 들어 SaveChanges을(를) 호출할 때 한 번에 적용할 수 없습니다. 실제로 함수는 EF의 변경 추적기를 전혀 인식하지 못하며 상호 작용이 전혀 없습니다. 여기에는 몇 가지 중요한 결과가 있습니다.

다음 코드를 생각해 봅시다.

// 1. Query the blog with the name `SomeBlog`. Since EF queries are tracking by default, the Blog is now tracked by EF's change tracker.
var blog = context.Blogs.Single(b => b.Name == "SomeBlog");

// 2. Increase the rating of all blogs in the database by one. This executes immediately.
context.Blogs.ExecuteUpdate(setters => setters.SetProperty(b => b.Rating, b => b.Rating + 1));

// 3. Increase the rating of `SomeBlog` by two. This modifies the .NET `Rating` property and is not yet persisted to the database.
blog.Rating += 2;

// 4. Persist tracked changes to the database.
context.SaveChanges();

결정적으로 ExecuteUpdate이(가) 호출되고 데이터베이스에서 모든 블로그가 업데이트되면 EF의 변경 추적기가 업데이트되지않으며, 추적된 .NET 인스턴스에는 쿼리된 시점부터 원래 등급 값이 계속 있습니다. 블로그의 등급이 원래 5라고 가정해 보겠습니다. 세 번째 줄이 실행된 후 데이터베이스의 등급은 이제 (ExecuteUpdate(으)로 인해) 6인 반면 추적된 .NET 인스턴스의 등급은 7입니다. SaveChanges이(가) 호출되면 EF는 새 값 7이 원래 값 5와 다르다는 것을 감지하고 변경 내용을 유지합니다. ExecuteUpdate에 의해 수행된 변경 내용은 덮어쓰여지고 고려되지 않습니다.

따라서 일반적으로 ExecuteUpdate/ExecuteDelete을(를) 통해 추적된 SaveChanges수정 내용과 추적되지 않은 수정 내용을 모두 혼합하지 않는 것이 좋습니다.

트랜잭션

위에서 덧붙이자면 ExecuteUpdateExecuteDelete이(가) 호출되는 트랜잭션을 암시적으로 시작하지 않는다는 것을 이해하는 것이 중요합니다. 다음 코드를 생각해 봅시다.

context.Blogs.ExecuteUpdate(/* some update */);
context.Blogs.ExecuteUpdate(/* another update */);

var blog = context.Blogs.Single(b => b.Name == "SomeBlog");
blog.Rating += 2;
context.SaveChanges();

ExecuteUpdate 호출을 사용하면 단일 SQL UPDATE이(가) 데이터베이스로 전송됩니다. 트랜잭션이 만들어지지 않으므로 모든 종류의 실패로 인해 두 번째 ExecuteUpdate이(가) 성공적으로 완료되지 못하는 경우 첫 번째 트랜잭션의 효과는 데이터베이스에 계속 유지됩니다. 실제로 위의 네 가지 작업(ExecuteUpdate의 호출 2개, 쿼리 및 SaveChanges)은 각각 자체 트랜잭션 내에서 실행됩니다. 단일 트랜잭션에서 여러 작업을 래핑하려면 DatabaseFacade을(를) 사용하여 트랜잭션을 명시적으로 시작합니다.

using (var transaction = context.Database.BeginTransaction())
{
    context.Blogs.ExecuteUpdate(/* some update */);
    context.Blogs.ExecuteUpdate(/* another update */);

    ...
}

트랜잭션 처리에 대한 자세한 내용은 트랜잭션 사용을 참조하세요.

영향을 받는 동시성 제어 및 행

SaveChanges은(는) 동시성 토큰을 사용하여 행을 로드한 시점과 변경 내용을 저장하는 순간 사이에 행이 변경되지 않도록 하는 자동 동시성 제어를 제공합니다. ExecuteUpdateExecuteDelete은(는) 변경 추적기와 상호 작용하지 않으므로 동시성 제어를 자동으로 적용할 수 없습니다.

그러나 이러한 메서드는 모두 작업의 영향을 받은 행 수를 반환합니다. 이는 동시성 제어를 직접 구현하는 데 특히 유용할 수 있습니다.

// (load the ID and concurrency token for a Blog in the database)

var numUpdated = context.Blogs
    .Where(b => b.Id == id && b.ConcurrencyToken == concurrencyToken)
    .ExecuteUpdate(/* ... */);
if (numUpdated == 0)
{
    throw new Exception("Update failed!");
}

이 코드에서는 LINQ Where 연산자를 사용하여 특정 블로그에 업데이트를 적용하고 동시성 토큰에 특정 값(예: 데이터베이스에서 블로그를 쿼리할 때 본 값)이 있는 경우에만 적용합니다. 그런 다음 ExecuteUpdate에 의해 실제로 업데이트된 행 수를 확인합니다. 결과가 0이면 업데이트된 행은 없으며 동시 업데이트의 결과로 동시성 토큰이 변경되었을 수 있습니다.

제한 사항

  • 현재 업데이트 및 삭제만 지원되며, 삽입은 DbSet<TEntity>.AddSaveChanges()을(를) 통해 수행해야 합니다.
  • SQL UPDATE 및 DELETE 문을 사용하면 영향을 받는 행에 대한 원래 열 값을 검색할 수 있지만 현재 ExecuteUpdateExecuteDelete은(는) 지원하지 않습니다.
  • 이러한 메서드의 여러 호출은 일괄 처리할 수 없습니다. 각 호출은 데이터베이스에 대한 자체 왕복을 수행합니다.
  • 데이터베이스는 일반적으로 UPDATE 또는 DELETE를 사용하여 단일 테이블만 수정할 수 있도록 합니다.
  • 이러한 메서드는 현재 관계형 데이터베이스 공급자에서만 작동합니다.

추가 리소스