Как установить значение свойства класса для установки сгенерированного значения идентификатора другого класса при вставке в базу данных? - PullRequest
6 голосов
/ 04 июля 2019

Мне сложно описать это словами, поэтому я буду использовать некоторый код, чтобы объяснить себя и свою проблему. Так что представьте, у меня есть два класса ClassA и ClassB:

class ClassA
{
    public int ClassAId { get; set; }
    public string Name { get; set; }
}

[Owned]
class ClassAOwned
{
    public int ClassAId { get; set; }
    public string Name { get; set; }
}

class ClassB
{
    public int ClassBId { get; set; }
    public string Action { get; set; }
    public ClassAOwned ClassA { get; set; }
}

, как вы можете видеть ClassB содержит ClassA, но у меня есть другой класс для него ClassAOwned, потому что я хочу, чтобы ClassB владел ClassA (сведет его столбцы в таблицу ClassB), но также имеет ClassA DbSet как отдельная таблица (и, насколько я понимаю, класс сущностей не может принадлежать и иметь собственный DbSet одновременно), поэтому мне пришлось использовать 2 разных класса. Вот мой контекст, чтобы было легче понять:

class TestContext : DbContext
{
    public DbSet<ClassA> ClassAs { get; set; }
    public DbSet<ClassB> ClassBs { get; set; }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder.UseInMemoryDatabase("TestContext");
    }
}

Теперь моя проблема возникает, когда я пытаюсь одновременно вставить ClassA и ClassB в контекст, и мне нужно сопоставить их значения ClassAId, которые генерирует поставщик базы данных:

var testContext = new TestContext();
var classA = new ClassA
{
    Name = "classAName"
};
var classB = new ClassB
{
    Action = "create",
    ClassA = new ClassAOwned
    {
        ClassAId = classA.ClassAId,
        Name = classA.Name
    }
};
testContext.ClassAs.Add(classA);
testContext.ClassBs.Add(classB);
classB.ClassA.ClassAId = classA.ClassAId;
testContext.SaveChanges();

при использовании InMemoryDatabase следующий вызов:

testContext.ClassAs.Add(classA);

фактически меняет classA.ClassAId на правильное сгенерированное значение, однако при использовании SQL-сервера classA.ClassAId устанавливается на int.MinValue, поэтому следующий вызов:

classB.ClassA.ClassAId = classA.ClassAId;

устанавливает classB.ClassA.ClassAId в int.MinValue. и последний звонок:

testContext.SaveChanges();

меняет classA.ClassAId на правильное сгенерированное значение, но classB.ClassA.ClassAId остается равным int.MinValue, и это значение вставляется в базу данных.

Мой вопрос: есть ли способ сообщить ядру EF, что при добавлении двух сущностей в контекст задайте свойству любое значение, сгенерированное для первичного ключа другой сущности? Таким образом, функциональность, которую я ищу, точно такая же, как добавление двух сущностей, у которых есть внешний ключ, за исключением того, что в данном случае это не действительно внешний ключ.

Простой обходной путь - установить «внешний ключ» (classB.ClassA.ClassAId) после testContext.SaveChanges() и снова сохранить изменения, но затем он становится двумя отдельными операциями, и что если вторая не удастся? База данных будет в недопустимом состоянии.

Ответы [ 3 ]

2 голосов
/ 08 июля 2019

Это возможно, но с некоторыми хитростями, которые работают с новейшим на данный момент EF Core 2.2 и могут перестать работать в 3.0+ (по крайней мере, необходимо проверить).

Во-первых, у есть , который нужно отобразить как отношения - другого пути просто нет. Это не обязательно должно быть реальное отношение к базе данных, просто оно должно быть таким с точки зрения модели EF Core.

Во-вторых, и это очень важно, поведение каскадного удаления должно быть установлено на Restrict, что в настоящее время означает принудительное применение в базе данных, но ничего не делает с отслеживаемыми связанными объектами в памяти.

Итак, давайте сделаем это с вами, образец. Оба вышеупомянутых отображения требуют плавной конфигурации, подобной этой:

modelBuilder.Entity<ClassB>().OwnsOne(e => e.ClassA)
    .HasOne<ClassA>().WithMany() // (1)
    .OnDelete(DeleteBehavior.Restrict); // (2)

Если вы используете миграции, сгенерированная миграция будет содержать что-то вроде этого:

protected override void Up(MigrationBuilder migrationBuilder)
{
    migrationBuilder.CreateTable(
        name: "ClassA",
        columns: table => new
        {
            ClassAId = table.Column<int>(nullable: false)
                .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn),
            Name = table.Column<string>(nullable: true)
        },
        constraints: table =>
        {
            table.PrimaryKey("PK_ClassA", x => x.ClassAId);
        });

    migrationBuilder.CreateTable(
        name: "ClassB",
        columns: table => new
        {
            ClassBId = table.Column<int>(nullable: false)
                .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn),
            Action = table.Column<string>(nullable: true),
            ClassA_ClassAId = table.Column<int>(nullable: false),
            ClassA_Name = table.Column<string>(nullable: true)
        },
        constraints: table =>
        {
            table.PrimaryKey("PK_ClassB", x => x.ClassBId);
            table.ForeignKey(
                name: "FK_ClassB_ClassA_ClassA_ClassAId",
                column: x => x.ClassA_ClassAId,
                principalTable: "ClassA",
                principalColumn: "ClassAId",
                onDelete: ReferentialAction.Restrict);
        });

    migrationBuilder.CreateIndex(
        name: "IX_ClassB_ClassA_ClassAId",
        table: "ClassB",
        column: "ClassA_ClassAId");
}

Отредактируйте его вручную и удалите команду ForeignKey (строку), так как вам не нужен настоящий FK. Вы также можете удалить соответствующую команду CreateIndex, хотя это не повредит.

И это все. Единственная важная вещь, которую вам нужно запомнить, - это использовать основное TableAId свойство только после того, как новая сущность была добавлена ​​(таким образом, отслежена) в контекст. т.е.

var testContext = new TestContext();
var classA = new ClassA
{
    Name = "classAName"
};
testContext.ClassAs.Add(classA); // <--
var classB = new ClassB
{
    Action = "create",
    ClassA = new ClassAOwned
    {
        ClassAId = classA.ClassAId, // <--
        Name = classA.Name
    }
};
testContext.ClassBs.Add(classB);
testContext.SaveChanges();

Будет сгенерировано временное отрицательное значение, но после SaveChanged оба идентификатора будут обновлены с фактическим значением, сгенерированным базой данных.

0 голосов
/ 08 июля 2019

После получения дополнительной информации один из способов реализации этого выглядит примерно так:

class A
{
    public int ID;
    public string data;
...
}

class HistoryA
{
    public int ID;
    public int AID;
    public string data;
    public DateTime updated;
...
}

Так что у меня есть один класс (класс A), содержащий текущую информацию, и другой historyA, ссылающийся на текущую (HistoryA.AID =A.ID) и сохранение всей предыдущей информации в HistoryA.

Таким образом, все свойства данных в A были продублированы в HistoryA.

0 голосов
/ 08 июля 2019

Ну ... Насколько я знаю, прямого способа сделать это нет.

Что бы я предложил, это пересмотреть решение о наличии двух экземпляров ClassA, одна из которых принадлежит, а другая - независимая. Это неэффективно и, очевидно, доставляет вам неприятности. Это также создает вероятность того, что две копии в конечном итоге выйдут из синхронизации.

Очевидный подход состоит в том, чтобы ClassA был независимым, а затем просто использовать «Включить», чтобы включить его в данные в ваших запросах. Все проблемы, которые вы описали здесь, исчезнут. Вместо того, чтобы пытаться синхронизировать две копии A, вы можете просто создать плоскую копию B, включая A, если / когда вам это нужно.

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