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





Ну... Насколько я знаю, прямого способа сделать это нет.
Я бы предложил пересмотреть решение о наличии двух копий ClassA, одной из которых является собственность, а другой — независимой. Это неэффективно и, очевидно, вызывает у вас проблемы. Это также создает вероятность того, что две копии в конечном итоге рассинхронизируются.
Очевидным подходом было бы сделать ClassA независимым, а затем просто использовать «Включить», чтобы включить его в данные в ваших запросах. Все трудности, которые вы здесь описали, исчезнут. Вместо того, чтобы пытаться синхронизировать две копии A, вы можете просто создать сглаженную копию B, включая A, если/когда вам это нужно.
Итак, что вам нужно сделать, это сохранить историю ClassA?
@Jonny хорошо, в основном да, ClassB содержит, какое действие (создано, обновлено, удалено) было предпринято на ClassA, когда, кем и каковы новые значения ClassA. Как я уже сказал, я придумал пару решений, которые могут работать, но не очень хороши, но я задаю этот вопрос в основном потому, что нашел интересной саму проблему. Конечно, вы можете предложить решения для проблемы истории, вы можете подумать о чем-то, о чем я не подумал.
После получения дополнительной информации одним из способов реализации этого является что-то вроде
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 оба идентификатора будут обновлены фактическим значением, сгенерированным базой данных.
Со структурой, которую вы описываете, я не могу достичь тех же целей, которые хочу.
DbSet<ClassA>будет взаимодействовать с использованием обычного CRUD, что означает, что значенияClassAмогут измениться при обновлении сущности.ClassBпредназначена для хранения истории каждогоClassAпредмета. Таким образом, используяDbSet<ClassB>, я могу точно знать, через что прошла каждаяClassAсущность и какие значения были обновлены. Я подумал о паре решений моей проблемы, ни одно из них не является очень чистым, но я только что нашел проблему в одном из этих решений интересной, поэтому я разместил ее здесь.