Вставка данных в несколько таблиц с транзакцией и откатом при сбое

#asp.net-mvc #entity-framework

Вопрос:

Каков наилучший способ хранения данных в базе данных при одновременной вставке нескольких классов в базу данных. У меня есть около 10 классов, где я хотел бы сохранить все данные вместе в базе данных.

Публикация некоторых классов с отношениями:

 Public class ClassMain
{
     public int ClassMainId { get; set; }
}

Public class ClassA
{
      public int ClassAId { get; set; }
      public int ClassMainId { get; set; }
      public string ClassAName { get; set; }
}

Public class ClassB
{
      public int ClassBId { get; set; }
      public int ClassMainId { get; set; }
      public string ClassBName { get; set; }
}
 

Как и выше, у меня есть несколько других таблиц с отношением

Я хотел бы вставить эти данные за один раз, если произойдет какой-либо сбой, я хотел бы откатить данные, в рамках Entity Framework или подхода к хранимым процедурам у нас есть подход для этого.

Я использую этот подход для достижения

https://dotnettutorials.net/lesson/unit-of-work-csharp-mvc/

Есть ли какой-то другой подход?

Ответ №1:

При условии, что они вставлены с использованием одного и того же DbContext и одного SaveChanges() вызова, они будут зафиксированы в одной и той же транзакции.

Если сущности связаны друг с другом с помощью FKs, то лучшим решением, при котором вы хотите, чтобы БД управляла PKS и автоматически разрешала FKS, было бы настроить свойства навигации и добавить подчиненные сущности в объекты верхнего уровня, прежде чем добавлять объекты верхнего уровня в соответствующие наборы баз данных и вызывать изменения сохранения.

В вашем случае вы хотели бы что-то вроде:

 public class ClassMain
{
    [Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    public int ClassMainId { get; set; }

    [InverseProperty("ClassMain")]
    public virtual ICollection<ClassA> ClassAs { get; set;} = new List<ClassA>();
    [InverseProperty("ClassMain")]
    public virtual ICollection<ClassB> ClassBs { get; set;} = new List<ClassB>();
}

public class ClassA
{
    [Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    public int ClassAId { get; set; }

    public int ClassMainId { get; set; }
    [ForeignKey("ClassMainId")]
    public virtual ClassMain ClassMain{ get; set; }
    public string ClassAName { get; set; }
}

Public class ClassB
{
    [Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    public int ClassBId { get; set; }

    public int ClassMainId { get; set; }
    [ForeignKey("ClassMainId")]
    public virtual ClassMain ClassMain{ get; set; }

    public string ClassBName { get; set; }
}
 

С EF Core 5 идентификатор класса FK в классах A и B является необязательным. Ядро EF может просто использовать свойства тени для отображения, а не отображать их как свойства. (Рекомендуется)

Теперь, когда вы переходите к созданию домена классов и связанных с ним A и B, вы можете просто создать новые экземпляры, заполнив различные поля, и добавить объект верхнего уровня в набор баз данных.

 using(var context = new AppDbContext())
{
    var newMain = new ClassMain
    {
        // ... set main properties, but don't worry about PK

        ClassA = new ClassA { ClassAName = someAName },
        ClassB = new ClassB { ClassBName = someBName }
    };
    context.ClassMains.Add(newMain);
    context.SaveChanges();
}
 

Вы также можете сделать это без идентификаторов PKS, где вы можете задать PKs/FKs в коде, хотя вы несете ответственность за то, чтобы они были уникальными. Настроив свойства навигации, EF позаботится о связывании FKS, когда все будет сохранено. Они будут экономить все вместе или вообще ничего не будут экономить.

Правка: Если вы не хотите, чтобы ClassMain содержал коллекции для экземпляров ClassA и ClassB, вы можете удалить эти коллекции и атрибуты обратных свойств. Заполнение экземпляров будет больше похоже на:

 using(var context = new AppDbContext())
{
    var newMain = new ClassMain();
    var classA = new ClassA { ClassAName = someAName, ClassMain = newMain };
    var classB = new ClassB { ClassBName = someBName, ClassMain = newMain };

    context.ClassAs.Add(classA);
    context.ClassBs.Add(classB);
    context.SaveChanges();
}
 

В этом случае экземпляр ClassMain будет сохранен вместе с классами A и B.

Комментарии:

1. Необходимо ли создавать это public class ClassMain { [Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)] public int ClassMainId { get; set; } [InverseProperty("ClassMain")] public virtual ICollection<ClassA> ClassAs { get; set;} = new List<ClassA>(); [InverseProperty("ClassMain")] public virtual ICollection<ClassB> ClassBs { get; set;} = new List<ClassB>(); }

2. Если вы хотите, чтобы только ClassA и ClassB ссылались на домен классов, чтобы он не содержал список каждого из них, это, безусловно, возможно. Просто создайте ClassMain, ClassA и ClassB, установите ссылку ClassMain в каждом из них на один экземпляр ClassMain, добавьте экземпляры ClassA и B в соответствующие наборы баз данных и вызовите SaveChanges() . Я расширил ответ, чтобы обрисовать этот сценарий.

Ответ №2:

Если у вас есть связи между таблицами, решение, которое сказал @Steve Py, является лучшим. Но если у вас нет никаких отношений между сущностями, вы можете использовать явные транзакции.

     using var context = new AppDbContext();
    using var transaction = context.Database.BeginTransaction();

    var newMain = new ClassMain();
    context.ClassMains.Add(newMain);
    context.SaveChanges();

    var newClassA = new ClassA
    {
        ClassMainId = newMain.ClassMainId
    };

    context.ClassAs.Add(newClassA);

    var newClassB = new ClassB
    {
        ClassMainId = newMain.ClassMainId
    };

    context.ClassBs.Add(newClassB);

    context.SaveChanges();

    transaction.Commit();