我正在使用Entity Framework 6 DB First和SQL Server表,每个表都有一个uniqueidentifier
主键。这些表在主键列上具有默认值,将其设置为newid()
。我已相应更新了我的.edmx,将这些列的StoreGeneratedPattern
设置为Identity
。所以我可以创建新记录,将它们添加到我的数据库上下文中,并自动生成ID。但现在我需要保存具有特定ID的新记录。我读过this article,说明在使用int identity PK列时保存之前必须执行SET IDENTITY_INSERT dbo.[TableName] ON
。由于我的是Guid而不是实际上是一个标识列,所以基本上已经完成了。然而,即使在我的C#中我将ID设置为正确的Guid,该值甚至不作为参数传递给生成的SQL插入,并且SQL Server为主键生成新的ID。
我需要能够同时:
我有#1。如何使用特定主键插入新记录?
修改
保存代码摘录(注意accountMemberSpec.ID是我想成为AccountMember主键的特定Guid值):
IDbContextScopeFactory dbContextFactory = new DbContextScopeFactory();
using (var dbContextScope = dbContextFactory.Create())
{
//Save the Account
dbAccountMember = CRMEntity<AccountMember>.GetOrCreate(accountMemberSpec.ID);
dbAccountMember.fk_AccountID = accountMemberSpec.AccountID;
dbAccountMember.fk_PersonID = accountMemberSpec.PersonID;
dbContextScope.SaveChanges();
}
-
public class CRMEntity<T> where T : CrmEntityBase, IGuid
{
public static T GetOrCreate(Guid id)
{
T entity;
CRMEntityAccess<T> entities = new CRMEntityAccess<T>();
//Get or create the address
entity = (id == Guid.Empty) ? null : entities.GetSingle(id, null);
if (entity == null)
{
entity = Activator.CreateInstance<T>();
entity.ID = id;
entity = new CRMEntityAccess<T>().AddNew(entity);
}
return entity;
}
}
-
public class CRMEntityAccess<T> where T : class, ICrmEntity, IGuid
{
public virtual T AddNew(T newEntity)
{
return DBContext.Set<T>().Add(newEntity);
}
}
这是为此记录的生成的SQL:
DECLARE @generated_keys table([pk_AccountMemberID] uniqueidentifier)
INSERT[dbo].[AccountMembers]
([fk_PersonID], [fk_AccountID], [fk_FacilityID])
OUTPUT inserted.[pk_AccountMemberID] INTO @generated_keys
VALUES(@0, @1, @2)
SELECT t.[pk_AccountMemberID], t.[CreatedDate], t.[LastModifiedDate]
FROM @generated_keys AS g JOIN [dbo].[AccountMembers] AS t ON g.[pk_AccountMemberID] = t.[pk_AccountMemberID]
WHERE @@ROWCOUNT > 0
-- @0: '731e680c-1fd6-42d7-9fb3-ff5d36ab80d0' (Type = Guid)
-- @1: 'f6626a39-5de0-48e2-a82a-3cc31c59d4b9' (Type = Guid)
-- @2: '127527c0-42a6-40ee-aebd-88355f7ffa05' (Type = Guid)
答案 0 :(得分:1)
我看到了两个挑战:
Id
字段成为具有自动生成值的标识将阻止您指定自己的GUID。最简单的解决方案:
Id
是PK, Id
生成新的Guid。示例模型
public class Person
{
public Person()
{
this.Id = Guid.NewGuid();
}
public Guid Id { get; set; }
}
用法
// "Auto id"
var person1 = new Person();
// Manual
var person2 = new Person
{
Id = new Guid("5d7aead1-e8de-4099-a035-4d17abb794b7")
}
这将满足您的两个需求,同时保持数据库的安全。唯一的缺点是你必须为所有型号做到这一点。
如果你采用这种方法,我宁愿在模型上看到一个工厂方法,它会给我带有默认值的对象(填充Id
)并消除默认构造函数。恕我直言,在默认构造函数中隐藏默认值设置器永远不是一件好事。我宁愿让我的工厂方法为我做,并知道新对象填充了默认值(意图)。
public class Person
{
public Guid Id { get; set; }
public static Person Create()
{
return new Person { Id = Guid.NewGuid() };
}
}
用法
// New person with default values (new Id)
var person1 = Person.Create();
// Empty Guid Id
var person2 = new Person();
// Manually populated Id
var person3 = new Person { Id = Guid.NewGuid() };
答案 1 :(得分:1)
解决方案可能是覆盖DbContext SaveChanges。在此函数中,查找要为其指定Id的DbSets的所有添加条目。
如果尚未指定Id,请指定一个,如果已指定:使用指定的。
覆盖所有SaveChanges:
public override void SaveChanges()
{
GenerateIds();
return base.SaveChanges();
}
public override async Task<int> SaveChangesAsync()
{
GenerateIds();
return await base.SaveChangesAsync();
}
public override async Task<int> SaveChangesAsync(System.Threading CancellationToken token)
{
GenerateIds();
return await base.SaveChangesAsync(token);
}
GenerateIds应检查您是否已为添加的条目提供了ID。如果没有,请提供一个。
我不确定所有DbSets是否应该具有所请求的功能,或者只有一些功能。要检查主键是否已填满,我需要知道主键的标识符。
我在您的班级CRMEntity
中看到您知道每个T
都有一个ID,这是因为此ID位于CRMEntityBase
或IGuid
,我们假设它在IGuid
。如果它在CRMEntityBase
中,则相应地更改以下内容。
以下是小步骤;如果需要,你可以创建一个大的LINQ。
private void GenerateIds()
{
// fetch all added entries that have IGuid
IEnumerable<IGuid> addedIGuidEntries = this.ChangeTracker.Entries()
.Where(entry => entry.State == EntityState.Added)
.OfType<IGuid>()
// if IGuid.Id is default: generate a new Id, otherwise leave it
foreach (IGuid entry in addedIGuidEntries)
{
if (entry.Id == default(Guid)
// no value provided yet: provide it now
entry.Id = GenerateGuidId() // TODO: implement function
// else: Id already provided; use this Id.
}
}
就是这样。因为所有IGuid对象现在都具有非默认ID(预定义或在GenerateId中生成),EF将使用该Id。
添加:HasDatabaseGeneratedOption
正如xr280xr在其中一条评论中指出的那样,我忘了你必须告诉实体框架实体框架不应该(总是)生成一个Id。
作为一个例子,我使用一个带有Blogs和Posts的简单数据库。博客和帖子之间的一对多关系。为了表明这个想法不依赖于GUID,主键很长。
// If an entity class is derived from ISelfGeneratedId,
// entity framework should not generate Ids
interface ISelfGeneratedId
{
public long Id {get; set;}
}
class Blog : ISelfGeneratedId
{
public long Id {get; set;} // Primary key
// a Blog has zero or more Posts:
public virtual ICollection><Post> Posts {get; set;}
public string Author {get; set;}
...
}
class Post : ISelfGeneratedId
{
public long Id {get; set;} // Primary Key
// every Post belongs to one Blog:
public long BlogId {get; set;}
public virtual Blog Blog {get; set;}
public string Title {get; set;}
...
}
现在有趣的部分:流畅的API,通知实体框架已经生成了主键的值。
我更喜欢流畅的API avobe使用属性,因为使用流畅的API允许我在不同的数据库模型中重用实体类,只需重写Dbcontext.OnModelCreating。
例如,在某些数据库中,我喜欢我的DateTime对象是DateTime2,而在某些数据库中,我需要它们是简单的DateTime。有时我想要自己生成的ID,有时候(比如在单元测试中)我不需要它。
class MyDbContext : Dbcontext
{
public DbSet<Blog> Blogs {get; set;}
public DbSet<Post> Posts {get; set;}
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
// Entity framework should not generate Id for Blogs:
modelBuilder.Entity<Blog>()
.Property(blog => blog.Id)
.HasDatabaseGeneratedOption(DatabaseGeneratedOption.None);
// Entity framework should not generate Id for Posts:
modelBuilder.Entity<Blog>()
.Property(blog => blog.Id)
.HasDatabaseGeneratedOption(DatabaseGeneratedOption.None);
... // other fluent API
}
SaveChanges与我上面写的类似。 GenerateIds略有不同。在这个例子中,我没有问题,有时Id已经填充。实现ISelfGeneratedId的每个添加元素都应生成Id
private void GenerateIds()
{
// fetch all added entries that implement ISelfGeneratedId
var addedIdEntries = this.ChangeTracker.Entries()
.Where(entry => entry.State == EntityState.Added)
.OfType<ISelfGeneratedId>()
foreach (ISelfGeneratedId entry in addedIdEntries)
{
entry.Id = this.GenerateId() ;// TODO: implement function
// now you see why I need the interface:
// I need to know the primary key
}
}
对于那些正在寻找一个整洁的Id生成器的人:我经常使用与Twitter相同的生成器,一个可以处理多个服务器的生成器,没有每个人都可以从主键猜测添加了多少项的问题。
答案 2 :(得分:0)
我认为这个问题没有真正的答案......
正如How can I force entity framework to insert identity columns?所说,您可以启用模式#2,但它会打破#1。
using (var dataContext = new DataModelContainer()) using (var transaction = dataContext.Database.BeginTransaction()) { var user = new User() { ID = id, Name = "John" }; dataContext.Database.ExecuteSqlCommand("SET IDENTITY_INSERT [dbo].[User] ON"); dataContext.User.Add(user); dataContext.SaveChanges(); dataContext.Database.ExecuteSqlCommand("SET IDENTITY_INSERT [dbo].[User] OFF"); transaction.Commit(); }
您应该在模型设计器中将标识列的StoreGeneratedPattern属性值从Identity更改为None。
注意,将StoreGeneratedPattern更改为None将无法插入没有指定id的对象
正如您所看到的,如果没有自行设置ID,您将无法再插入。
但是,如果你看起来很光明:Guid.NewGuid()
将允许你在没有数据库生成功能的情况下创建一个新的GUID。
答案 3 :(得分:0)
解决方案是:编写自己的插入查询。我已经组织了一个快速项目来测试这个,所以这个示例与您的域名无关,但您可以使用该域名。
using (var ctx = new Model())
{
var ent = new MyEntity
{
Id = Guid.Empty,
Name = "Test"
};
try
{
var result = ctx.Database.ExecuteSqlCommand("INSERT INTO MyEntities (Id, Name) VALUES ( @p0, @p1 )", ent.Id, ent.Name);
}
catch (SqlException e)
{
Console.WriteLine("id already exists");
}
}
ExecuteSqlCommand
返回&#34;受影响的行&#34; (在这种情况下为1),或者为重复键抛出异常。