最近在開發記錄感想功能的時候用到了1對1的數據關系,具體情況是這樣的,有這樣兩個1對1的類型
public class Item
{
public int Id { get; set; }
public string Title { get; set; }
public Note Note { get; set; }
}
public class Note
{
public int Id { get; set; }
public string Content { get; set; }
public int ItemId { get; set; }
public Item Item { get; set; }
public bool Deleted { get; set; }
}
它們的1對1關系配置如下:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Note>(e =>
{
e.HasOne(x => x.Item).WithOne(x => x.Note).HasForeignKey<Note>(x => x.ItemId);
e.HasQueryFilter(x => !x.Deleted);
});
}
Note
是軟刪除的,這里配置了一個QueryFilter
然后我們用dotnet-ef
命令構建數據庫,生成的腳本如下:
IF OBJECT_ID(N'[__EFMigrationsHistory]') IS NULL
BEGIN
CREATE TABLE [__EFMigrationsHistory] (
[MigrationId] nvarchar(150) NOT NULL,
[ProductVersion] nvarchar(32) NOT NULL,
CONSTRAINT [PK___EFMigrationsHistory] PRIMARY KEY ([MigrationId])
);
END;
GO
CREATE TABLE [Items] (
[Id] int NOT NULL IDENTITY,
[Title] nvarchar(max) NULL,
CONSTRAINT [PK_Items] PRIMARY KEY ([Id])
);
GO
CREATE TABLE [Notes] (
[Id] int NOT NULL IDENTITY,
[Content] nvarchar(max) NULL,
[ItemId] int NOT NULL,
[Deleted] bit NOT NULL,
CONSTRAINT [PK_Notes] PRIMARY KEY ([Id]),
CONSTRAINT [FK_Notes_Items_ItemId] FOREIGN KEY ([ItemId]) REFERENCES [Items] ([Id]) ON DELETE CASCADE
);
GO
CREATE UNIQUE INDEX [IX_Notes_ItemId] ON [Notes] ([ItemId]);
GO
INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion])
VALUES (N'20190813141425_InitEntities', N'2.2.6-servicing-10079');
GO
再造一條數據,方便測試
USE [demo]
GO
INSERT INTO [dbo].[Items]
([Title])
VALUES
('a')
GO
不出意外的話,這個Item
的Id
會是1
業務代碼如下:
[ApiController]
[Route("[controller]")]
public class NoteController : ControllerBase
{
private readonly DemoContext _db;
public NoteController(DemoContext db)
{
_db = db;
}
[HttpGet]
public IEnumerable<Note> Get()
{
return _db.Notes.ToList();
}
[HttpPost]
public void Post()
{
var item = _db.Items.Include(x => x.Note).FirstOrDefault(x => x.Id == 1);
if (item != null)
{
item.AddNote(DateTime.Now.ToString("F"));
_db.SaveChanges();
}
}
[HttpDelete]
public void Delete()
{
var item = _db.Items.Include(x => x.Note).FirstOrDefault(x => x.Id == 1);
if (item != null)
{
item.DeleteNote();
_db.SaveChanges();
}
}
}
就是對Id==1
的Item
新增/修改/刪除Note
有這樣一個很簡單的場景,用戶先新增了Note
,然后刪除Note
,再想新增Note
,這時候你就會發現數據庫報錯了:Note
違反了唯一性約束。
由於Note
是軟刪除的,所有當再次新增Note
的時候就會出現重復的ItemId
。
解決這個問題的思路也很簡單,只需要把這個外鍵的唯一性約束更改為過濾掉Deleted
的數據進行約束。
更改關系配置
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Note>(e =>
{
e.HasOne(x => x.Item).WithOne(x => x.Note).HasForeignKey<Note>(x =x.ItemId);
e.HasQueryFilter(x => !x.Deleted);
e.HasIndex(x => x.ItemId).IsUnique().HasFilter($"[{nameof(Note.Deleted)}]=0");
});
}
給這個ItemId
的唯一性約束加一個條件e.HasIndex(x => x.ItemId).IsUnique().HasFilter($"[{nameof(Note.Deleted)}]=0");
再用dotnet-ef
命令生成的數據庫更新腳本,如下:
DROP INDEX [IX_Notes_ItemId] ON [Notes];
GO
CREATE UNIQUE INDEX [IX_Notes_ItemId] ON [Notes] ([ItemId]) WHERE [Deleted]=0;
GO
INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion])
VALUES (N'20190813144240_FilterIndex', N'2.2.6-servicing-10079');
GO
用有條件的INDEX
替換了原先的INDEX
現在再次執行先前的業務,新增,刪除,再次新增就正常了。
完整代碼github