CREATE TABLE Blogs (
blog_id int IDENTITY PRIMARY KEY,
blog_name varchar(50),
blog_author varchar(50)
);
CREATE TABLE Posts (
post_id int IDENTITY PRIMARY KEY,
post_title varchar(50),
post_contents text,
post_categories varchar(50),
post_blogid int FOREIGN KEY REFERENCES Blogs(blog_id),
post_created datetime,
post_published bit
);
[ActiveRecord("Blogs")]
public class Blog : ActiveRecordBase {
[PrimaryKey(PrimaryKeyType.Native, "blog_id")]
public int Id { get; set; }
[Property("blog_name")] public string Name { get; set; }
[Property("blog_author")] public string Author { get; set; }
[HasMany(typeof(Post), Table = "posts", ColumnKey = "post_blogid")]
public IList Posts { get; set; }
}
[ActiveRecord("Posts")]
public class Post : ActiveRecordBase {
[PrimaryKey(PrimaryKeyType.Native, "post_id")]
public int Id { get; set; }
[Property("post_title")] public string Title { get; set; }
[Property(Column = "post_contents", ColumnType = "StringClob")] public string Contents { get; set; }
[Property("post_categories")] public string Category { get; set; }
[Property("post_created")] public DateTime Created { get; set; }
[Property("post_published")] public bool Published { get; set; }
[BelongsTo("post_blogid")]
public Blog Blog { get; set; }
}
| 屬性 | 說明 | 常用值 |
|---|---|---|
| Cascade | 級聯操作範圍 | None / SaveUpdate / Delete / All / AllDeleteOrphan |
| Inverse | 是否由關聯端維護外鍵(建議 true) | true / false |
| Lazy | 延遲載入 | true / false |
| Where | 固定過濾條件(資料表欄位名) | "post_published = 1" |
| BatchSize | 批次抓取筆數(優化 N+1) | 10, 25 |
| 屬性 | 說明 | 常用值 |
|---|---|---|
| Column | 外鍵欄位名 | "post_blogid" |
| Cascade | 級聯操作 | None / SaveUpdate / Delete / All |
| NotNull | 外鍵不可為 Null | true / false |
| OuterJoin | 急切外連接抓取 | Auto / True / False |
| Update/Insert | 是否包含在 UPDATE/INSERT SQL | true / false |
[Test]
public void TestCascadingSave() {
var blog = new Blog { Name = "Terrylee's Tech Space", Author = "Terrylee" };
using (var tran = new TransactionScope()) {
try {
blog.Create();
for (int i = 1; i <= 10; i++) {
var post = new Post {
Title = $"This is my {i} post",
Category = "Castle",
Blog = blog // 設定關聯
};
post.Save();
}
tran.VoteCommit();
} catch { tran.VoteRollBack(); }
}
}
需在 HasMany 設 Cascade = ManyRelationCascadeEnum.SaveUpdate:
[HasMany(typeof(Post), Table = "posts", ColumnKey = "post_blogid",
Cascade = ManyRelationCascadeEnum.SaveUpdate)]
public IList Posts { get; set; }
[Test]
public void TestCascadingUpdate() {
var blog = Blog.Find(5);
using (var tran = new TransactionScope()) {
try {
for (int i = 1; i <= 5; i++) {
var post = new Post {
Title = $"New post {i}",
Category = "Castle",
Blog = blog
};
blog.Posts.Add(post); // 加入集合
// post.Save() 可省略,SaveUpdate 級聯會自動持久化
}
blog.Update();
tran.VoteCommit();
} catch { tran.VoteRollBack(); }
}
}
設 Cascade = ManyRelationCascadeEnum.AllDeleteOrphan:
[Test]
public void TestCascadingUpdateDel() {
var blog = Blog.Find(7);
var before = blog.Posts.Count;
using (var tran = new TransactionScope()) {
try {
blog.Posts.RemoveAt(0); // 移除第一篇
blog.Update(); // 自動刪除對應 Post 記錄
tran.VoteCommit();
} catch { tran.VoteRollBack(); }
}
Assert.AreEqual(before - 1, Blog.Find(7).Posts.Count);
}
設 Cascade = ManyRelationCascadeEnum.All 或 AllDeleteOrphan:
[Test]
public void TestCascadingDelete() {
var blog = Blog.Find(5);
using (var tran = new TransactionScope()) {
try {
blog.Delete(); // 連帶刪除所有 Posts
tran.VoteCommit();
} catch { tran.VoteRollBack(); }
}
}
在 HasMany 加上 Inverse = true,表示外鍵由 BelongsTo 端(Post.Blog)維護:
[HasMany(typeof(Post), Table = "posts", ColumnKey = "post_blogid",
Inverse = true, Cascade = ManyRelationCascadeEnum.AllDeleteOrphan)]
public IList Posts { get; set; }
這樣 Blog.Save/Update 時不會嘗試更新 post_blogid,由 Post 端統一管理,避免重複 UPDATE 與死結。
若從 Post 角度看,屬於一個 Blog,即為 BelongsTo;從 Blog 角度看,擁有多個 Posts,即為 HasMany。映射設定完全一致,無需額外學習。
一對多映射核心在於 HasMany(集合端)與 BelongsTo(參考端)雙向配合。關鍵在於:正確設定 ColumnKey/Column 對應外鍵、配置 Inverse=true 避免雙向維護衝突、根據業務需求選擇 Cascade 模式(推薦 AllDeleteOrphan 實現完整生命週期管理)。掌握這些,即可靈活處理主從關聯業務。