我有一个名为 ClubApplicationUser 的模型,它是 Club 和 ApplicationUser 之间的桥梁,该模型是 Identity User 的扩展模型。 strong>模型:
public class ClubApplicationUser
{
public Guid ClubID { get; set; }
public Club Club { get; set; }
public string Id { get; set; }
public ApplicationUser ApplicationUser { get; set; }
public DateTime DateCreated { get; set; }
public string CreatedBy { get; set; }
public DateTime LastDateModified { get; set; }
public string LastModifiedBy { get; set; }
public DateTime? DateDeleted { get; set; }
public string DeletedBy { get; set; }
public bool IsDeleted { get; set; }
[Timestamp]
public byte[] RowVersion { get; set; }
[ForeignKey("CreatedBy")]
public ApplicationUser ClubApplicationCreatedUser { get; set; }
[ForeignKey("LastModifiedBy")]
public ApplicationUser ClubApplicationLastModifiedUser { get; set; }
}
在ApplicationDBContext-OnModelCreating中,我们定义了关系:
builder.Entity<ClubApplicationUser>()
.HasKey(bc => new { bc.ClubID, bc.Id });
builder.Entity<ClubApplicationUser>()
.HasOne(bc => bc.Club)
.WithMany(b => b.ClubApplicationUsers)
.HasForeignKey(bc => bc.ClubID);
builder.Entity<ClubApplicationUser>()
.HasOne(bc => bc.ApplicationUser)
.WithMany(c => c.ClubApplicationUsers)
.HasForeignKey(bc => bc.Id);
我们有一个问题,无法更新,并且出现错误:
InvalidOperationException:实体类型上的属性'ClubID' 'ClubApplicationUser'是密钥的一部分,因此无法进行修改或 标记为已修改。用以下方法更改现有实体的主体 标识外键首先删除依赖项并调用 然后,“ SaveChanges”将依赖项与新的主体相关联。
这是AssignClub.cs:
public class AssignClubUserModel : ClubNamePageModel
{
private readonly AthlosifyWebArchery.Data.ApplicationDbContext _context;
public AssignClubUserModel(AthlosifyWebArchery.Data.ApplicationDbContext context)
{
_context = context;
}
public class AssignClubUserViewModel<ApplicationUser>
{
public string FirstName { get; set; }
public string LastName { get; set; }
public string UserName { get; set; }
public Guid SelectedClubID { get; set; }
public byte[] RowVersion { get; set; }
}
[BindProperty]
public AssignClubUserViewModel<ApplicationUser> AssignClubUser { get; set; }
public SelectList ClubNameSL { get; set; }
public async Task<IActionResult> OnGetAsync(Guid? id)
{
if (id == null)
return NotFound();
var user = await _context.Users
.Include(u => u.ClubApplicationUsers)
.Where(t => t.Id == id.ToString())
.Select(t => new AssignClubUserViewModel<ApplicationUser>
{
FirstName = t.FirstName,
LastName = t.LastName,
UserName = t.UserName,
SelectedClubID = t.ClubApplicationUsers.ElementAt(0).ClubID,
RowVersion = t.RowVersion
}).SingleAsync();
AssignClubUser = user;
// Use strongly typed data rather than ViewData.
ClubNameSL = new SelectList(_context.Club, "ClubID", "Name");
//PopulateClubsDropDownList(_context);
return Page();
}
public async Task<IActionResult> OnPostAsync(Guid id)
{
if (!ModelState.IsValid)
{
return Page();
}
// 1st approach:
// Modify the bridge model directly
var clubApplicationUserToUpdate = await _context.ClubApplicationUser
.FirstOrDefaultAsync(m => m.Id == id.ToString());
if (clubApplicationUserToUpdate == null)
{
return await HandleDeletedUser();
}
_context.Entry(clubApplicationUserToUpdate)
.Property("RowVersion").OriginalValue = AssignClubUser.RowVersion;
_context.Entry(clubApplicationUserToUpdate)
.Property("ClubID").OriginalValue = AssignClubUser.SelectedClubID;
await _context.SaveChangesAsync();
// 2nd approach:
// Soft -Delete and Add
// Did the soft-deleting and managed to add a new one BUT then die the roll back (adding the old one)
// Result: Violation of PRIMARY KEY constraint 'PK_ClubApplicationUser'.
// Cannot insert duplicate key in object
// Due to duplicate key
/*var clubApplicatonUserToRemove = await _context.ClubApplicationUser
.FirstOrDefaultAsync(m => m.Id == id.ToString());
ClubApplicationUser clubApplicatonUserToAdd = new ClubApplicationUser();
clubApplicatonUserToAdd.Id = id.ToString();
clubApplicatonUserToAdd.ClubID = AssignClubUser.SelectedClubID;
//_context.Entry(clubApplicatonUserToRemove)
// .Property("RowVersion").OriginalValue = User.RowVersion;
if (clubApplicatonUserToRemove != null)
{
_context.ClubApplicationUser.Remove(clubApplicatonUserToRemove);
await _context.SaveChangesAsync();
_context.ClubApplicationUser.Add(clubApplicatonUserToAdd);
await _context.SaveChangesAsync();
}*/
return Page();
}
private async Task<IActionResult> HandleDeletedUser()
{
//ClubA deletedClubApplicationUser = new ApplicationUser();
//ModelState.AddModelError(string.Empty,
// "Unable to save. The user was deleted by another user.");
//ClubNameSL = new SelectList(_context.Roles, "Id", "Name", User.UserRoles.ElementAt(0).RoleId);
return Page();
}
private async Task setDbErrorMessage(ApplicationUser dbValues,
ApplicationUser clientValues, ApplicationDbContext context)
{
if (dbValues.FirstName != clientValues.FirstName)
{
ModelState.AddModelError("User.FirstName",
$"Current value: {dbValues.FirstName}");
}
if (dbValues.LastName != clientValues.LastName)
{
ModelState.AddModelError("User.LastName",
$"Current value: {dbValues.LastName}");
}
if (dbValues.Email != clientValues.Email)
{
ModelState.AddModelError("User.Email",
$"Current value: {dbValues.Email}");
}
ModelState.AddModelError(string.Empty,
"The record you attempted to edit "
+ "was modified by another user after you. The "
+ "edit operation was canceled and the current values in the database "
+ "have been displayed. If you still want to edit this record, click "
+ "the Save button again.");
}
}
...和AssignClub.cshtml:
@page
@model AthlosifyWebArchery.Pages.Administrators.Users.AssignClubUserModel
@{
ViewData["Title"] = "Assign Club";
}
<h2>Assign Club</h2>
<h4>User</h4>
<hr />
<div class="row">
<div class="col-md-4">
<form method="post">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div class="form-group">
<label asp-for="AssignClubUser.FirstName" class="control-label">
</label>
<input asp-for="AssignClubUser.FirstName" disabled class="form-
control" />
</div>
<div class="form-group">
<label asp-for="AssignClubUser.LastName" class="control-label">
</label>
<input asp-for="AssignClubUser.LastName" disabled class="form-control" />
</div>
<div class="form-group">
<label asp-for="AssignClubUser.UserName" class="control-label">
</label>
<input asp-for="AssignClubUser.UserName" disabled class="form-control" />
</div>
<div class="form-group">
<label class="control-label">Club</label>
<select asp-for="AssignClubUser.SelectedClubID" class="form-control"
asp-items="@Model.ClubNameSL">
<option value="">-- Select Club --</option>
</select>
<span asp-validation-for="AssignClubUser.SelectedClubID" class="text-danger"></span>
</div>
<div class="form-group">
<input type="submit" value="Save" class="btn btn-default" />
</div>
</form>
</div>
</div>
<div>
<a asp-page="./Index">Back to List</a>
</div>
@section Scripts {
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}
环境: .Net Core 2.2 剃刀页面
更新-1:
如果我们直接通过以下操作在数据库上进行更新:
UPDATE [ClubApplicationUser]
SET ClubID = '85715C34-AFC6-4498-DA7F-08D66CAE7A01'
WHERE Id = 'ecbd27b4-03bc-4b99-82b3-76d9aa5bc7fc'
我们可以更新此问题。因此,它看起来像.Net核心模型中的约束。
答案 0 :(得分:2)
我认为最好的解决方案是您需要删除并插入而不是更新,尽管实际上给您的ClubApplicationUser可能意味着对IsDeleted字段进行更新,而不是实际执行Delete。
如果您以自己的域的逻辑来考虑,我认为用户通常不会从一个俱乐部的成员变成另一个俱乐部的成员,而是会离开(删除)一个俱乐部并加入(插入)另一个俱乐部。
尽管我可以提出另一个可以进行更新的域,所以我认为这不是一个很好的通用论点。
以下代码显示问题的简化版本。您可以看到测试允许插入和删除,但由于更新失败
public class Club
{
public int Id { get; set; }
public string Name { get; set; }
public IList<ClubUser> Users { get; set; }
}
public class User
{
public int Id { get; set; }
public string Name { get; set; }
public IList<ClubUser> Clubs { get; set; }
}
public class ClubUser
{
public int ClubID { get; set; }
public Club Club { get; set; }
public int Id { get; set; }
public User User { get; set; }
public string Extra { get; set; }
}
public class ApplicationDbContext : Microsoft.EntityFrameworkCore.DbContext
{
public DbSet<User> Users { get; set; }
public DbSet<Club> Clubs { get; set; }
public DbSet<ClubUser> ClubUsers { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseSqlServer(@"Server=.;Database=Spike;Trusted_Connection=True;");
}
protected override void OnModelCreating(Microsoft.EntityFrameworkCore.ModelBuilder builder)
{
builder.Entity<Club>()
.HasKey(c => c.Id );
builder.Entity<User>()
.HasKey(c => c.Id );
builder.Entity<ClubUser>()
.HasKey(cu => new { cu.ClubID, cu.Id });
builder.Entity<ClubUser>()
.HasOne<Club>(cu => cu.Club)
.WithMany(u => u.Users)
.HasForeignKey(bc => bc.ClubID);
builder.Entity<ClubUser>()
.HasOne<User>(cu => cu.User)
.WithMany(c => c.Clubs)
.HasForeignKey(cu => cu.Id);
}
}
[TestClass]
public class ManyToMany
{
[TestMethod]
public void DeleteAndInsert()
{
var context = new ApplicationDbContext();
var clubusers = context.ClubUsers;
var clubs = context.Clubs;
var users = context.Users;
var original = clubusers.First();
clubusers.Remove(original);
var newClubUser = new ClubUser
{
Club = clubs.Last(),
User = users.First(),
Extra = "Another"
};
clubusers.Add(newClubUser);
context.SaveChanges();
}
[TestMethod]
public void Update()
{
var context = new ApplicationDbContext();
var clubusers = context.ClubUsers;
var clubs = context.Clubs;
var users = context.Users;
var update = clubusers.First();
update.Club = clubs.Last();
update.Extra = "Changed";
Assert.ThrowsException<InvalidOperationException>( () => context.SaveChanges());
}
}
要初始化测试数据库:
ALTER TABLE [dbo].[ClubUsers] DROP CONSTRAINT [FK_ClubUser_User]
GO
ALTER TABLE [dbo].[ClubUsers] DROP CONSTRAINT [FK_ClubUser_Club]
GO
DROP TABLE [dbo].[ClubUsers]
GO
DROP TABLE [dbo].[Clubs]
GO
DROP TABLE [dbo].[Users]
GO
CREATE TABLE [dbo].[Clubs](
[Id] [int] IDENTITY(1,1) NOT NULL,
[Name] [varchar](50) NOT NULL,
CONSTRAINT [PK_Club] PRIMARY KEY CLUSTERED
(
[Id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
GO
CREATE TABLE [dbo].[Users](
[Id] [int] IDENTITY(1,1) NOT NULL,
[Name] [varchar](50) NOT NULL,
CONSTRAINT [PK_User] PRIMARY KEY CLUSTERED
(
[Id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
GO
CREATE TABLE [dbo].[ClubUsers](
[ClubId] [int] NOT NULL,
[Id] [int] NOT NULL,
[Extra] [varchar](50) NOT NULL,
CONSTRAINT [PK_ClubUser] PRIMARY KEY CLUSTERED
(
[ClubId] ASC,
[Id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
GO
ALTER TABLE [dbo].[ClubUsers] WITH CHECK ADD CONSTRAINT [FK_ClubUser_Club] FOREIGN KEY([ClubId])
REFERENCES [dbo].[Clubs] ([Id])
GO
ALTER TABLE [dbo].[ClubUsers] CHECK CONSTRAINT [FK_ClubUser_Club]
GO
ALTER TABLE [dbo].[ClubUsers] WITH CHECK ADD CONSTRAINT [FK_ClubUser_User] FOREIGN KEY([Id])
REFERENCES [dbo].[Users] ([Id])
GO
ALTER TABLE [dbo].[ClubUsers] CHECK CONSTRAINT [FK_ClubUser_User]
GO
INSERT Clubs(Name)
VALUES ('GlenEagles');
INSERT Clubs(Name)
VALUES ('StAndrews');
INSERT Clubs(Name)
VALUES ('Wentworth');
INSERT dbo.[Users](Name)
VALUES ('Pete');
INSERT dbo.[Users](Name)
VALUES ('Dave');
INSERT ClubUsers(ClubId, Id, Extra)
VALUES (1,1, 'Hello');
答案 1 :(得分:2)
*无效,请参见评论*
我的第三个选择是最快实现的,但是我不确定所有的含义。
如果将OnModelCreating更改为设置索引而不是键
即
builder.Entity<ClubUser>()
.HasKey(cu => new { cu.ClubID, cu.Id });
成为
builder.Entity<ClubUser>()
.HasIndex(cu => new { cu.ClubID, cu.Id });
该更新现在可以正常工作,但是您在ClubUser上没有密钥,这可能会导致其他问题。
答案 2 :(得分:2)
关于:
ClubApplicationUsers表中的InvalidOperationException:实体类型上的属性'ClubID' “ ClubApplicationUser”是密钥的一部分...
PrimaryKey既是ClubID又是ID。 您不能仅通过ID更改现有记录。
例如:
var clubApplicationUserToUpdate = await _context.ClubApplicationUser
.FirstOrDefaultAsync(m => m.Id == id.ToString());
必须是这样的:
var clubApplicationUserToUpdate = await _context.ClubApplicationUser
.FirstOrDefaultAsync(m => m.Id == id.ToString() && m.ClubID == AssignClubUser.SelectedClubID.ToString());
或者:
var clubApplicationUsersToUpdate = await _context.ClubApplicationUser
.Where(m => m.Id == id.ToString()).ToList();
关于:
第二种方法:
...
结果:违反了PRIMARY KEY约束'PK_ClubApplicationUser'。
我将举例说明:
Clubs: 1, 2, 3
ApplicationUsers: A, B, C
ClubApplicationUser: A1, A2
尝试删除A1,然后添加A2-表示A2已经存在。
解决方案更接近第二种方法:
public async Task<IActionResult> OnPostAsync(Guid id)
{
if (!this.ModelState.IsValid)
{
return Page();
}
//delete all club memberships and add new one
var clubApplicatonUsersToRemove = await _context.ClubApplicationUser
.Where(m => m.Id == id.ToString()).ToList();
foreach (var clubApplicatonUser in clubApplicatonUsersToRemove)
{
_context.ClubApplicationUser.Remove(clubApplicatonUser);
}
_context.ClubApplicationUser.Add(new ClubApplicationUser()
{
Id = id.ToString(),
ClubID = AssignClubUser.SelectedClubID
});
await _context.SaveChangesAsync();
return Page();
}
如果您不想删除任何内容,而只需添加新记录:
public async Task<IActionResult> OnPostAsync(Guid id)
{
if (!this.ModelState.IsValid)
{
return Page();
}
// dont delete, just add new one
var clubApplicatonUserExists = await _context.ClubApplicationUser
.Where(m => m.Id == id.ToString() && m.ClubID == AssignClubUser.SelectedClubID).FirstOrDefaultAsync();
if (clubApplicatonUserExists == null)
{
_context.ClubApplicationUser.Add(new ClubApplicationUser()
{
Id = id.ToString(),
ClubID = AssignClubUser.SelectedClubID
});
await _context.SaveChangesAsync();
}
return Page();
}
答案 3 :(得分:1)
如果您对数据库架构有控制权,另一种解决方案是向链接表添加一个多余的键。
然后,如果您需要更新俱乐部或用户,则无需更改实体唯一标识符,因此将被允许。