Многократное изменение базы данных в ошибке выброса области - PullRequest
0 голосов
/ 23 апреля 2019

У меня есть метод, который удаляет старые файлы из базы данных в зависимости от некоторых условий:

using (var scope = new TransactionScope(TransactionScopeOption.Required, new System.TimeSpan(0, 15, 0)))
{
    using (var ctx = new ElectronicFileEntities())
    {
        var deleteFilesTime = Int32.Parse(_appSettings["UnFiledDocumentsRetainTime"]);

        var cutOffTime = DateTime.Now.AddHours(-deleteFilesTime);
        var documentsToDelete = ctx.Documents.Where(o => !o.IsDeleted && !o.IsFiled && o.LastModified < cutOffTime);

        foreach (var document in documentsToDelete)
        {
            _log.InfoFormat("Document to be deleted  {0}", document.DocumentId);
            document.Comment = "Deleted by loader service - not filed in time";
            ctx.DeleteDocument(document.DocumentId, DateTime.Now, 0);
            ctx.InsertDocumentHistory(document.DocumentId, "DELETE");
        }

        ctx.SaveChanges();
    }

    scope.Complete();
}

Все хорошо в среде UAT, но начал получать ошибки в работе.

Ошибка произошла во время метода: DeleteOldUnFiledDocuments.
Сообщение об ошибке: Ошибка основного провайдера при открытии.

Трассировка стека:
в System.Data.Entity.Core.EntityClient.EntityConnection.Open ()
в System.Data.Entity.Core.Objects.ObjectContext.EnsureConnection (Boolean shouldMonitorTransactions)
в System.Data.Entity.Core.Objects.ObjectContext.ExecuteInTransaction [T] (операция Func 1 func, IDbExecutionStrategy executionStrategy, Boolean startLocalTransaction, Boolean releaseConnectionOnSuccess) at System.Data.Entity.Core.Objects.ObjectContext.<>c__DisplayClass4b.<ExecuteFunction>b__49() at System.Data.Entity.SqlServer.DefaultSqlExecutionStrategy.Execute[TResult](Func 1) вSystem.Data.Entity.Core.Objects.ObjectContext.ExecuteFunction (параметры String functionName, ObjectParameter []) в ElectronicFile.Entities.ElectronicCustomerFileEntities.DeleteDocument (Nullable 1 documentId, Nullable 1 lastModified, NullableOnectionObBectionObenceObDectionObenceObDectionObjects, DbB, DЗаключениеОбъединения, повторная попытка обращения,) в System.Data.ProviderBase.DbConnectionFactory.TryGetConnection (DbConnection owningConnection, TaskCompletionSource 1 retry, DbConnectionOptions userOptions, DbConnectionInternal oldConnection, DbConnectionInternal& connection) at System.Data.ProviderBase.DbConnectionInternal.TryOpenConnectionInternal(DbConnection outerConnection, DbConnectionFactory connectionFactory, TaskCompletionSource 1, повторная попытка, DbConnectionOptions userOptions) в System.Data.SqlClient.SqlConnection.TryOpenInner (TaskCompletionSource 1 retry) at System.Data.SqlClient.SqlConnection.TryOpen(TaskCompletionSource 1 повтор) в System.Data.SqlClient.SqlConnate.Onity. System.ata.Infrastructure.Interception.InternalDispatcher 1.Dispatch[TTarget,TInterceptionContext](TTarget target, Action 2 операция, TInterceptionContext interceptionContext, действие 3 executing, Action 3 выполнено) в System.Data.Entity.Infrastructure.Interception.DbConnectionDispatcher.Open (соединение DbConnection, DbInterceptionContext interceptionContextE)..DefaultSqlExecutionStrategy. <> C__DisplayClass1.b__0 () в System.Data.Entity.SqlServer.DefaultSqlExecutionStrategy.Execute [TResult] (операция Func`1) в System.Data.Entity.Core.EntityClient.EntityConnection.EntityConnection.

Я думаю, это может быть из-за того, что цикл foreach пытается работать с несколькими записями (Количество удаляемых документов = 7412) Как лучше всего решить эту проблему

Это мой сохраненный рrocedure:

CREATE PROCEDURE [dbo].[DeleteDocument]
     @DocumentId     INT,
     @LastModified   DATETIME,
     @LastModifiedBy INT
AS
BEGIN
    SET NOCOUNT ON;

    BEGIN TRY
        UPDATE [Document]
        SET IsDeleted = 1,
            LastModified = @LastModified,
            LastModifiedBy = @LastModifiedBy
        WHERE
            DocumentId = @DocumentId

    END TRY
    BEGIN CATCH
        DECLARE @ErrorMessage NVARCHAR(4000); 
        DECLARE @ErrorSeverity INT; 
        DECLARE @ErrorState INT; 

        SELECT
            @ErrorMessage = ERROR_MESSAGE(), 
            @ErrorSeverity = ERROR_SEVERITY(), 
            @ErrorState = ERROR_STATE(); 

        RAISERROR (@ErrorMessage, @ErrorSeverity, @ErrorState); 
    END CATCH
END

Это хранимая процедура для вставки в таблицу истории

CREATE PROCEDURE [dbo].[InsertDocumentHistory]
     @DocumentId INT,
     @ChangeType VARCHAR(10)    
AS
BEGIN
    SET NOCOUNT ON;

    BEGIN TRY
        DECLARE @ChangeDateTime AS DATETIME

        SELECT @ChangeDateTime = LastModified 
        FROM [Document] 
        WHERE DocumentId = @DocumentId

        UPDATE DocumentHistory
        SET ActiveTo = @ChangeDateTime
        WHERE DocumentId = @DocumentId
          AND ActiveTo IS NULL

        INSERT INTO DocumentHistory (DocumentId, DocumentTypeId, InTrayId, CustomerFileId, ReferenceId,
                                     FileDataId, FileDataType, FileDataSize, FileNoteReference,
                                     EffectiveStartDate, EffectiveEndDate, Comment, OriginalFileName, 
                                     IsFiled, IsFlatten, IsVerified, IsDeleted,
                                     ExternalPartyId, ExternalPartyTypeId,
                                     ChangeBy, ChangeType, ActiveFrom) 
            SELECT 
                DocumentId, DocumentTypeId, InTrayId, CustomerFileId, ReferenceId,
                FileDataId, FileDataType, FileDataSize, FileNoteReference,
                EffectiveStartDate, EffectiveEndDate, Comment, OriginalFileName,
                IsFiled, IsFlatten, IsVerified, IsDeleted,
                ExternalPartyId, ExternalPartyTypeId, LastModifiedBy,
                @ChangeType, @ChangeDateTime
            FROM
                [Document]
            WHERE
                DocumentId = @DocumentId
    END TRY
    BEGIN CATCH
        DECLARE @ErrorMessage NVARCHAR(4000); 
        DECLARE @ErrorSeverity INT; 
        DECLARE @ErrorState INT; 

        SELECT
            @ErrorMessage = ERROR_MESSAGE(), 
            @ErrorSeverity = ERROR_SEVERITY(), 
            @ErrorState = ERROR_STATE(); 

        RAISERROR (@ErrorMessage, @ErrorSeverity, @ErrorState); 
    END CATCH
END

Ответы [ 2 ]

0 голосов
/ 03 мая 2019

Окончательный рабочий раствор.переместил коммит внутри цикла for

  foreach (Document document in documentsToDelete)
                    {
                        using (var dbContextTransaction = ctx.Database.BeginTransaction())
                        {
                            try
                            {
                                document.Comment = "Deleted by loader service - not filed in time";
                                ctx.SaveChanges();

                                ctx.DeleteDocument(document.DocumentId, DateTime.Now, 0);
                                ctx.SaveChanges();

                                ctx.InsertDocumentHistory(document.DocumentId, "DELETE");
                                ctx.SaveChanges();

                                dbContextTransaction.Commit();
                            }
                            catch(Exception ex)
                            {
                                dbContextTransaction.Rollback();
                                var message = string.Format("Error occurred during method: {0}. Error message: {1}. Stack trace: {2}. Inner exception: {3}", methodName, ex.Message, ex.StackTrace, ex.InnerException);
                                _log.ErrorFormat(message);
                            }

                        }
                    }

`

0 голосов
/ 23 апреля 2019

Несколько вещей приходят на ум: Во-первых, это утверждение:

var documentsToDelete = ctx.Documents.Where(o => !o.IsDeleted && !o.IsFiled && o.LastModified < cutOffTime);

Это приведет к IQueryable, который будет повторяться, в то время как ваш контекст ожидает сохранения изменений несколько раз в каждой итерации цикла. Если вы уверены, что количество записей будет разумным, попробуйте добавить .ToList() в конце этого или .Take(100) или что-то подобное, чтобы получить разумное число. Мне было бы любопытно, что на самом деле делает ваш контекст ElectronicFilesEntities DeleteDocument (), например, повторно извлекает документ по идентификатору.

Я предполагаю, что этот метод DeleteDocument устанавливает IsDeleted = true и устанавливает метку удаленного времени?

Следующим шагом будет удаление нескольких вызовов SaveChanges. Какова цель TransactionScope? Потому что DbContext уже по своей природе будет использовать транзакцию для операций, которые вы выполняете против него. Область транзакции была бы более применимой, если вы хотите координировать изменения между несколькими DbContexts или между DbContext и другой операцией, которая участвует в области транзакции.

using (var ctx = new ElectronicFileEntities())
{
    var deleteFilesTime = Int32.Parse(_appSettings["UnFiledDocumentsRetainTime"]);
    var cutOffTime = DateTime.Now.AddHours(-deleteFilesTime);
    var documentsToDelete = ctx.Documents
        .Where(o => !o.IsDeleted && !o.IsFiled && o.LastModified < cutOffTime).ToList();

    foreach (var document in documentsToDelete)
    {
        document.Comment = "Deleted by loader service - not filed in time";
        ctx.DeleteDocument(document.DocumentId, DateTime.Now, 0);
        ctx.InsertDocumentHistory(document.DocumentId, "DELETE");
    }
    ctx.SaveChanges();
}

Лучшим шаблоном IMO для чего-то подобного является DDD (Domain-Driven Design). Документ содержит метод «Delete», который отвечает за накопление всех соответствующих действий.

1017 * Т.е. *

public void Delete(string comment)
{
    Comment = comment;
    IsDeleted = true;
    DeletedAt = DateTime.Now;
    History.Add(new DocumentHistory
    {
       Action = "DELETE",
       // ...
    });
}

Тогда логика для пометки документов как удаленных становится:

using (var ctx = new ElectronicFileEntities())
{
    var deleteFilesTime = Int32.Parse(_appSettings["UnFiledDocumentsRetainTime"]);
    var cutOffTime = DateTime.Now.AddHours(-deleteFilesTime);
    var documentsToDelete = ctx.Documents
        .Include(o => o.History)
        .Where(o => !o.IsDeleted && !o.IsFiled && o.LastModified < cutOffTime).ToList();

    foreach (var document in documentsToDelete)
        document.Delete("Deleted by loader service - not filed in time");

    ctx.SaveChanges();
}

Предостережение заключается в том, что история документа является коллекцией под документом, и в этом случае мы бы хотели загрузить ее или рискнуть отложенной загрузкой при добавлении новой истории. Если история недоступна для объекта или представляет потенциально большой набор для извлечения данных, и мы хотим разделить его, тогда я бы предложил использовать шаблон репозитория, а не использовать DbContext, если только этот контекст не ограничен для обслуживания только эти связанные объекты / операции. В противном случае число методов, предоставляемых контекстом для выполнения таких действий, как удаление, станет очень большим.

Если вы идете по пути контекста к удалению, где документ не имеет прямой ссылки на историю документа, то я бы рекомендовал объединить удаление в DDD-подобную операцию:

// In Document entity...
public void Delete(string comment)
{
    Comment = comment;
    IsDeleted = true;
    DeletedAt = DateTime.Now;
    // no history reference.
}

// in DbContext
public Document DeleteDocument(Document document, string comment)
{
    document.Delete(comment);
    DocumentHistory.Add(new DocumentHistory
    {
       Document = document,
       Action = "DELETE",
       // ...
    });
}

затем в вашем коде для перебора документов для удаления:

foreach (var document in documentsToDelete)
    ctx.DeleteDocument(document, "Deleted by loader service - not filed in time");

Хорошая статья о DDD подходит с EF: https://www.thereformedprogrammer.net/creating-domain-driven-design-entity-classes-with-entity-framework-core/

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...