我有一个SQL Server数据库作为Web服务后端,在移动前端有一个本地SQLite数据库。
两者都使用带有 TpH 方法的EF Core 2.2.4并共享大多数C#类,但是-当然-后端类由于需要管理的原因而具有更多的属性。为了对差异进行建模,使用了条件属性和流畅的API(请参见下面的代码)。
主要思想是在后端创建数据库内容,然后将摘录存储在前端数据库中。
到最后(简化),本地数据库应仅存储单元列表。
每个单元包含一个练习列表(n:m)。
练习是从抽象类派生的,某些练习类型可以包含存储在额外表中的所需工具(1:n)列表。
每次练习都是唯一的,因此我可以将其主键用于n:m单元练习关系。
另一方面,一个工具可能被多个练习引用,这给将带有练习的单元添加到本地前端数据库带来了麻烦。
问题在于,练习工具1:n映射也使用该工具的主键,因为这是后端创建它的方式,因此,如果前端上的工具的主键已经存在,则前端数据库Add()
调用失败,并显示
SQLite错误19:“唯一约束失败:Tool.Id”
所以,问题是:(重新)设计前端模型的最简单方法是什么,以便我仅插入后端生成的单元列表
下面是示例代码
namespace Test
{
#if FRONTEND
[Table("TestUnit")]
#endif
public class TestUnit
{
public Guid Id { get; set; } // Use values from backend
public string Name { get; set; }
public List<TestExerciseSequence> ExerciseSequences { get; set; }
}
public class TestExerciseSequence
{
public Guid UnitId { get; set; }
public TestUnit Unit { get; set; }
public Guid ExerciseId { get; set; }
public TestExercise Exercise { get; set; }
}
#if FRONTEND
[Table("TestExercise")]
#endif
public abstract class TestExercise
{
public Guid Id { get; set; }
public string Name { get; set; }
public string Discriminator { get; set; }
public List<TestExerciseSequence> ExerciseSequences { get; set; }
}
public class TestExerciseType1 : TestExercise
{
public TestExerciseToolType ToolType { get; set; }
}
public class TestExerciseToolType
{
public int Id { get; set; }
public string Name { get; set; }
public List<TestExerciseType1> ExerciseTypes { get; set; }
}
public class TestLocalDBContext : DbContext
{
// All locally stored units
public DbSet<TestUnit> Units { get; set; }
private const string databaseName = "sqlite.db";
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
string databasePath;
switch (Device.RuntimePlatform)
{
case Device.Android:
databasePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Personal), databaseName);
break;
default:
throw new NotImplementedException("Platform not supported");
}
#if FRONTEND
optionsBuilder.UseSqlite($"Filename={databasePath}")
.EnableDetailedErrors()
.EnableSensitiveDataLogging()
;
#endif
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
// n : m relationship between 'Unit' to 'Exercise'
modelBuilder.Entity<TestExerciseSequence>()
.HasKey(us => new { us.UnitId, us.ExerciseId });
modelBuilder.Entity<TestExerciseSequence>()
.HasOne<TestUnit>(us => us.Unit)
.WithMany(u => u.ExerciseSequences)
.HasForeignKey(us => us.UnitId);
modelBuilder.Entity<TestExerciseSequence>()
.HasOne<TestExercise>(us => us.Exercise)
.WithMany(e => e.ExerciseSequences)
.HasForeignKey(us => us.ExerciseId);
#if FRONTEND
// TestExerciseToolType causes troubles
modelBuilder.Entity<TestExerciseToolType>()
.Property(c => c.Id)
.ValueGeneratedNever();
modelBuilder.Entity<TestExerciseToolType>()
.HasIndex(c => c.Id)
.IsUnique(true);
#endif
// 1 : n relationship between 'TestExerciseType1' to 'TestExerciseToolType'
modelBuilder.Entity<TestExerciseType1>()
.HasOne<TestExerciseToolType>(etmt => etmt.ToolType)
.WithMany(etmt => etmt.ExerciseTypes);
#if FRONTEND
// Copied from above
modelBuilder.Entity<TestExerciseToolType>()
.Property(c => c.Id)
.ValueGeneratedNever();
modelBuilder.Entity<TestExerciseToolType>()
.HasIndex(c => c.Id)
.IsUnique(true);
#endif
// Table-per-Hierarchy for 'Exercise'
modelBuilder.Entity<TestExercise>()
.HasDiscriminator<string>("Discriminator")
.HasValue<TestExerciseType1>("ExerciseTypeMovement")
;
modelBuilder.Entity<TestExercise>().Property("Discriminator").HasMaxLength(80);
// Unit - ExerciseSequence
modelBuilder.Entity<TestUnit>().HasMany(u => u.ExerciseSequences);
}
}
public class TestDBHelper<T> where T : TestLocalDBContext
{
protected TestLocalDBContext CreateContext()
{
var dbContext = (T)Activator.CreateInstance(typeof(T));
dbContext.Database.EnsureCreated();
dbContext.Database.Migrate();
return dbContext;
}
public void AddUnit(TestUnit u)
{
using (var context = CreateContext())
{
context.Units.Add(u);
context.SaveChanges();
}
}
}
}
和测试代码
private void DoTest()
{
var uId1 = Guid.NewGuid();
var uId2 = Guid.NewGuid();
var eId1 = Guid.NewGuid();
var eId2 = Guid.NewGuid();
var eId3 = Guid.NewGuid();
var eId4 = Guid.NewGuid();
var u1 = new TestUnit()
{
Id = uId1,
Name = "Unit1",
ExerciseSequences = new List<TestExerciseSequence>()
{
new TestExerciseSequence()
{
UnitId = uId1,
ExerciseId = eId1,
Exercise = new TestExerciseType1()
{
Id = eId1,
Discriminator = "TestExerciseType1",
Name = "E1",
ToolType = new TestExerciseToolType()
{
Id = 1,
Name = "M1"
}
}
},
new TestExerciseSequence()
{
UnitId = uId1,
ExerciseId = eId2,
Exercise = new TestExerciseType1()
{
Id = eId2,
Discriminator = "TestExerciseType1",
Name = "E2",
ToolType = new TestExerciseToolType()
{
Id = 2,
Name = "M2"
}
}
}
}
};
var u2 = new TestUnit()
{
Id = uId2,
Name = "Unit2",
ExerciseSequences = new List<TestExerciseSequence>()
{
new TestExerciseSequence()
{
UnitId = uId2,
ExerciseId = eId3,
Exercise = new TestExerciseType1()
{
Id = eId3,
Discriminator = "TestExerciseType1",
Name = "E3",
ToolType = new TestExerciseToolType()
{
Id = 3,
Name = "M3"
}
}
},
new TestExerciseSequence()
{
UnitId = uId2,
ExerciseId = eId4,
Exercise = new TestExerciseType1()
{
Id = eId4,
Discriminator = "TestExerciseType1",
Name = "E4",
ToolType = new TestExerciseToolType()
{
Id = 1, // Exception!
Name = "M1"
}
}
}
}
};
try
{
var database = new TestDBHelper<TestLocalDBContext>();
database.AddUnit(u1);
database.AddUnit(u2); // Exception
}
catch (Exception ex)
{
Debug.WriteLine($"Error {ex.Message}");
}
}
预先感谢
答案 0 :(得分:0)
使用实体时,上下文是实体的“所有者”。您可以“新建”一个实体,但是您必须尊重上下文期望由该实体控制。
当您执行以下操作时:
var parent1 = new Parent
{
ParentId = 1,
Child = new Child
{
ChildId = 1,
},
}
var parent2 = new Parent
{
ParentId = 2,
Child = new Child
{
ChildId = 1,
}
}
context.Parents.Add(parent1);
context.Parents.Add(parent2);
context.SaveChanges();
父级1将成功添加,但父级2将失败。之所以失败,是因为EF本质上会同时处理这两种情况,并将每个父母的孩子视为一个新实体。当它在父代1上遇到ID为1的“新”孩子时,您会遇到PK违规。
如果将实体设置为使用“身份”列,则可以避免此错误,但是父级1和2的子级引用将指向两个具有不同生成ID的不同记录。
为避免这种情况,在设置批量数据时,请先创建并关联您的子实体,然后再将其与父实体关联。例如:
var child1 = new Child { ChildId = 1 };
var parent1 = new Parent
{
ParentId = 1,
Child = child1
}
var parent2 = new Parent
{
ParentId = 2,
Child = child1
}
context.Parents.Add(parent1);
context.Parents.Add(parent2);
context.SaveChanges();
这样,父母双方都会引用同一个孩子,并且随着EF继续保留每个父母,它将从上下文中解析相同的孩子引用。
一对多关系也是如此:
parent1.Children.Add(child1)
而不是
parent1.Children.Add(new Child { ChildId = 1 });
如果您要创建实体并设置FK ID而不是引用,但是将那些实体作为普通实体使用(如果需要,请延迟加载引用),那么您应该使用DbSet.Create()
而不是new