Как установить значение свойства класса для сгенерированного значения идентификатора другого класса при вставке в базу данных?

Мне немного сложно выразить это словами, поэтому я воспользуюсь кодом, чтобы объяснить себя и свою проблему. Итак, представьте, что у меня есть два класса 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() и снова сохранить изменения, но тогда это становится двумя отдельными операциями, и что, если вторая не удастся? База данных будет в недопустимом состоянии.

Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
6
0
458
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 3

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

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

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

Со структурой, которую вы описываете, я не могу достичь тех же целей, которые хочу. DbSet<ClassA> будет взаимодействовать с использованием обычного CRUD, что означает, что значения ClassA могут измениться при обновлении сущности. ClassB предназначена для хранения истории каждого ClassA предмета. Таким образом, используя DbSet<ClassB>, я могу точно знать, через что прошла каждая ClassA сущность и какие значения были обновлены. Я подумал о паре решений моей проблемы, ни одно из них не является очень чистым, но я только что нашел проблему в одном из этих решений интересной, поэтому я разместил ее здесь.

ESipalis 08.07.2019 14:49

Итак, что вам нужно сделать, это сохранить историю ClassA?

Jonny 08.07.2019 15:20

@Jonny хорошо, в основном да, ClassB содержит, какое действие (создано, обновлено, удалено) было предпринято на ClassA, когда, кем и каковы новые значения ClassA. Как я уже сказал, я придумал пару решений, которые могут работать, но не очень хороши, но я задаю этот вопрос в основном потому, что нашел интересной саму проблему. Конечно, вы можете предложить решения для проблемы истории, вы можете подумать о чем-то, о чем я не подумал.

ESipalis 08.07.2019 16:08

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

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.

Ответ принят как подходящий

Это возможно, но с некоторыми хитростями, которые работают с последним на данный момент 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 оба идентификатора будут обновлены фактическим значением, сгенерированным базой данных.

Другие вопросы по теме