Hubungan banyak ke banyak
Hubungan banyak ke banyak digunakan ketika entitas angka dari satu jenis entitas dikaitkan dengan sejumlah entitas dengan jenis entitas yang sama atau lainnya. Misalnya, Post
dapat memiliki banyak , Tags
dan masing-masing Tag
dapat dikaitkan dengan sejumlah Posts
.
Memahami hubungan banyak ke banyak
Hubungan banyak ke banyak berbeda dari hubungan satu-ke-banyak dan satu-ke-satu karena hubungan tersebut tidak dapat diwakili dengan cara yang sederhana hanya menggunakan kunci asing. Sebagai gantinya, jenis entitas tambahan diperlukan untuk "menggabungkan" dua sisi hubungan. Ini dikenal sebagai "gabungkan jenis entitas" dan memetakan ke "tabel gabungan" dalam database relasional. Entitas dari jenis entitas gabungan ini berisi pasangan nilai kunci asing, di mana salah satu dari setiap pasangan menunjuk ke entitas di satu sisi hubungan, dan yang lain menunjuk ke entitas di sisi lain hubungan. Setiap entitas gabungan, dan oleh karena itu setiap baris dalam tabel gabungan, oleh karena itu mewakili satu hubungan antara jenis entitas dalam hubungan.
EF Core dapat menyembunyikan jenis entitas gabungan dan mengelolanya di belakang layar. Ini memungkinkan navigasi hubungan banyak ke banyak digunakan secara alami, menambahkan atau menghapus entitas dari setiap sisi sesuai kebutuhan. Namun, berguna untuk memahami apa yang terjadi di belakang layar sehingga perilaku keseluruhan mereka, dan khususnya pemetaan ke database relasional, masuk akal. Mari kita mulai dengan penyiapan skema database relasional untuk mewakili hubungan banyak ke banyak antara posting dan tag:
CREATE TABLE "Posts" (
"Id" INTEGER NOT NULL CONSTRAINT "PK_Posts" PRIMARY KEY AUTOINCREMENT);
CREATE TABLE "Tags" (
"Id" INTEGER NOT NULL CONSTRAINT "PK_Tags" PRIMARY KEY AUTOINCREMENT);
CREATE TABLE "PostTag" (
"PostsId" INTEGER NOT NULL,
"TagsId" INTEGER NOT NULL,
CONSTRAINT "PK_PostTag" PRIMARY KEY ("PostsId", "TagsId"),
CONSTRAINT "FK_PostTag_Posts_PostsId" FOREIGN KEY ("PostsId") REFERENCES "Posts" ("Id") ON DELETE CASCADE,
CONSTRAINT "FK_PostTag_Tags_TagsId" FOREIGN KEY ("TagsId") REFERENCES "Tags" ("Id") ON DELETE CASCADE);
Dalam skema ini, PostTag
adalah tabel gabungan. Ini berisi dua kolom: PostsId
, yang merupakan kunci asing ke kunci Posts
primer tabel, dan TagsId
, yang merupakan kunci asing untuk kunci Tags
primer tabel. Oleh karena itu, setiap baris dalam tabel ini mewakili hubungan antara satu Post
dan satu Tag
.
Pemetaan sederhana untuk skema ini di EF Core terdiri dari tiga jenis entitas--satu untuk setiap tabel. Jika masing-masing jenis entitas ini diwakili oleh kelas .NET, maka kelas tersebut mungkin terlihat sebagai berikut:
public class Post
{
public int Id { get; set; }
public List<PostTag> PostTags { get; } = [];
}
public class Tag
{
public int Id { get; set; }
public List<PostTag> PostTags { get; } = [];
}
public class PostTag
{
public int PostsId { get; set; }
public int TagsId { get; set; }
public Post Post { get; set; } = null!;
public Tag Tag { get; set; } = null!;
}
Perhatikan bahwa dalam pemetaan ini tidak ada hubungan banyak ke banyak, melainkan dua hubungan satu-ke-banyak, satu untuk masing-masing kunci asing yang ditentukan dalam tabel gabungan. Ini bukan cara yang tidak masuk akal untuk memetakan tabel ini, tetapi tidak mencerminkan niat tabel gabungan, yaitu mewakili hubungan banyak-ke-banyak tunggal, bukan dua hubungan satu-ke-banyak.
EF memungkinkan pemetaan yang lebih alami melalui pengenalan dua navigasi koleksi, satu pada Post
yang berisi terkait Tags
, dan inversi pada Tag
yang berisi terkait Posts
. Misalnya:
public class Post
{
public int Id { get; set; }
public List<PostTag> PostTags { get; } = [];
public List<Tag> Tags { get; } = [];
}
public class Tag
{
public int Id { get; set; }
public List<PostTag> PostTags { get; } = [];
public List<Post> Posts { get; } = [];
}
public class PostTag
{
public int PostsId { get; set; }
public int TagsId { get; set; }
public Post Post { get; set; } = null!;
public Tag Tag { get; set; } = null!;
}
Tip
Navigasi baru ini dikenal sebagai "lewati navigasi", karena melewati entitas gabungan untuk menyediakan akses langsung ke sisi lain dari hubungan banyak-ke-banyak.
Seperti yang ditunjukkan dalam contoh di bawah ini, hubungan banyak ke banyak dapat dipetakan dengan cara ini --yaitu, dengan kelas .NET untuk entitas gabungan, dan dengan kedua navigasi untuk dua hubungan satu-ke-banyak dan melewati navigasi yang diekspos pada jenis entitas. Namun, EF dapat mengelola entitas gabungan secara transparan, tanpa kelas .NET yang ditentukan untuk itu, dan tanpa navigasi untuk dua hubungan satu-ke-banyak. Misalnya:
public class Post
{
public int Id { get; set; }
public List<Tag> Tags { get; } = [];
}
public class Tag
{
public int Id { get; set; }
public List<Post> Posts { get; } = [];
}
Memang, konvensi pembuatan model EF akan, secara default, memetakan jenis dan Tag
yang Post
ditunjukkan di sini ke tiga tabel dalam skema database di bagian atas bagian ini. Pemetaan ini, tanpa penggunaan eksplisit dari jenis gabungan, adalah apa yang biasanya dimaksudkan dengan istilah "banyak-ke-banyak".
Contoh
Bagian berikut berisi contoh hubungan banyak ke banyak, termasuk konfigurasi yang diperlukan untuk mencapai setiap pemetaan.
Tip
Kode untuk semua contoh di bawah ini dapat ditemukan di ManyToMany.cs.
Dasar banyak-ke-banyak
Dalam kasus paling mendasar untuk banyak-ke-banyak, jenis entitas di setiap akhir hubungan keduanya memiliki navigasi koleksi. Misalnya:
public class Post
{
public int Id { get; set; }
public List<Tag> Tags { get; } = [];
}
public class Tag
{
public int Id { get; set; }
public List<Post> Posts { get; } = [];
}
Hubungan ini dipetakan oleh konvensi. Meskipun tidak diperlukan, konfigurasi eksplisit yang setara untuk hubungan ini ditunjukkan di bawah ini sebagai alat pembelajaran:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Post>()
.HasMany(e => e.Tags)
.WithMany(e => e.Posts);
}
Bahkan dengan konfigurasi eksplisit ini, banyak aspek hubungan masih dikonfigurasi oleh konvensi. Konfigurasi eksplisit yang lebih lengkap, sekali lagi untuk tujuan pembelajaran, adalah:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Post>()
.HasMany(e => e.Tags)
.WithMany(e => e.Posts)
.UsingEntity(
"PostTag",
l => l.HasOne(typeof(Tag)).WithMany().HasForeignKey("TagsId").HasPrincipalKey(nameof(Tag.Id)),
r => r.HasOne(typeof(Post)).WithMany().HasForeignKey("PostsId").HasPrincipalKey(nameof(Post.Id)),
j => j.HasKey("PostsId", "TagsId"));
}
Penting
Jangan mencoba mengonfigurasi semuanya sepenuhnya bahkan ketika tidak diperlukan. Seperti yang dapat dilihat di atas, kode menjadi rumit dengan cepat dan mudah untuk membuat kesalahan. Dan bahkan dalam contoh di atas ada banyak hal dalam model yang masih dikonfigurasi oleh konvensi. Tidak realistis untuk berpikir bahwa segala sesuatu dalam model EF selalu dapat dikonfigurasi sepenuhnya secara eksplisit.
Terlepas dari apakah hubungan dibangun oleh konvensi atau menggunakan salah satu konfigurasi eksplisit yang ditampilkan, skema yang dipetakan yang dihasilkan (menggunakan SQLite) adalah:
CREATE TABLE "Posts" (
"Id" INTEGER NOT NULL CONSTRAINT "PK_Posts" PRIMARY KEY AUTOINCREMENT);
CREATE TABLE "Tags" (
"Id" INTEGER NOT NULL CONSTRAINT "PK_Tags" PRIMARY KEY AUTOINCREMENT);
CREATE TABLE "PostTag" (
"PostsId" INTEGER NOT NULL,
"TagsId" INTEGER NOT NULL,
CONSTRAINT "PK_PostTag" PRIMARY KEY ("PostsId", "TagsId"),
CONSTRAINT "FK_PostTag_Posts_PostsId" FOREIGN KEY ("PostsId") REFERENCES "Posts" ("Id") ON DELETE CASCADE,
CONSTRAINT "FK_PostTag_Tags_TagsId" FOREIGN KEY ("TagsId") REFERENCES "Tags" ("Id") ON DELETE CASCADE);
Tip
Saat menggunakan alur Database First untuk membuat perancah DbContext dari database yang ada, EF Core 6 dan yang lebih baru mencari pola ini dalam skema database dan perancah memaparkan hubungan banyak ke banyak seperti yang dijelaskan dalam dokumen ini. Perilaku ini dapat diubah melalui penggunaan templat T4 kustom. Untuk opsi lain, lihat Hubungan banyak ke banyak tanpa entitas gabungan yang dipetakan sekarang dibuat perancah.
Penting
Saat ini, EF Core menggunakan untuk mewakili instans Dictionary<string, object>
entitas gabungan yang tidak ada kelas .NET yang telah dikonfigurasi. Namun, untuk meningkatkan performa, jenis yang berbeda dapat digunakan dalam rilis EF Core di masa mendatang. Jangan bergantung pada jenis Dictionary<string, object>
gabungan kecuali ini telah dikonfigurasi secara eksplisit.
Banyak ke banyak dengan tabel gabungan bernama
Dalam contoh sebelumnya, tabel gabungan dinamai PostTag
menurut konvensi. Ini dapat diberikan nama eksplisit dengan UsingEntity
. Misalnya:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Post>()
.HasMany(e => e.Tags)
.WithMany(e => e.Posts)
.UsingEntity("PostsToTagsJoinTable");
}
Segala sesuatu yang lain tentang pemetaan tetap sama, dengan hanya nama tabel gabungan yang berubah:
CREATE TABLE "PostsToTagsJoinTable" (
"PostsId" INTEGER NOT NULL,
"TagsId" INTEGER NOT NULL,
CONSTRAINT "PK_PostsToTagsJoinTable" PRIMARY KEY ("PostsId", "TagsId"),
CONSTRAINT "FK_PostsToTagsJoinTable_Posts_PostsId" FOREIGN KEY ("PostsId") REFERENCES "Posts" ("Id") ON DELETE CASCADE,
CONSTRAINT "FK_PostsToTagsJoinTable_Tags_TagsId" FOREIGN KEY ("TagsId") REFERENCES "Tags" ("Id") ON DELETE CASCADE);
Banyak ke banyak dengan nama kunci asing tabel gabungan
Mengikuti dari contoh sebelumnya, nama kolom kunci asing dalam tabel gabungan juga dapat diubah. Ada dua cara untuk melakukannya. Yang pertama adalah secara eksplisit menentukan nama properti kunci asing pada entitas gabungan. Misalnya:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Post>()
.HasMany(e => e.Tags)
.WithMany(e => e.Posts)
.UsingEntity(
l => l.HasOne(typeof(Tag)).WithMany().HasForeignKey("TagForeignKey"),
r => r.HasOne(typeof(Post)).WithMany().HasForeignKey("PostForeignKey"));
}
Cara kedua adalah meninggalkan properti dengan nama konvensi demi nama, tetapi kemudian memetakan properti ini ke nama kolom yang berbeda. Misalnya:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Post>()
.HasMany(e => e.Tags)
.WithMany(e => e.Posts)
.UsingEntity(
j =>
{
j.Property("PostsId").HasColumnName("PostForeignKey");
j.Property("TagsId").HasColumnName("TagForeignKey");
});
}
Dalam kedua kasus, pemetaan tetap sama, dengan hanya nama kolom kunci asing yang berubah:
CREATE TABLE "PostTag" (
"PostForeignKey" INTEGER NOT NULL,
"TagForeignKey" INTEGER NOT NULL,
CONSTRAINT "PK_PostTag" PRIMARY KEY ("PostForeignKey", "TagForeignKey"),
CONSTRAINT "FK_PostTag_Posts_PostForeignKey" FOREIGN KEY ("PostForeignKey") REFERENCES "Posts" ("Id") ON DELETE CASCADE,
CONSTRAINT "FK_PostTag_Tags_TagForeignKey" FOREIGN KEY ("TagForeignKey") REFERENCES "Tags" ("Id") ON DELETE CASCADE);
Tip
Meskipun tidak ditampilkan di sini, dua contoh sebelumnya dapat digabungkan untuk memetakan perubahan nama tabel gabungan dan nama kolom kunci asingnya.
Banyak ke banyak dengan kelas untuk entitas gabungan
Sejauh ini dalam contoh, tabel gabungan telah secara otomatis dipetakan ke jenis entitas jenis bersama. Ini menghapus kebutuhan akan kelas khusus yang akan dibuat untuk jenis entitas. Namun, dapat berguna untuk memiliki kelas seperti itu sehingga dapat direferensikan dengan mudah, terutama ketika navigasi atau payload ditambahkan ke kelas , seperti yang ditunjukkan pada contoh selanjutnya di bawah ini. Untuk melakukan ini, pertama-tama buat jenis PostTag
untuk entitas gabungan selain jenis yang ada untuk Post
dan Tag
:
public class Post
{
public int Id { get; set; }
public List<Tag> Tags { get; } = [];
}
public class Tag
{
public int Id { get; set; }
public List<Post> Posts { get; } = [];
}
public class PostTag
{
public int PostId { get; set; }
public int TagId { get; set; }
}
Tip
Kelas dapat memiliki nama apa pun, tetapi adalah umum untuk menggabungkan nama jenis di kedua akhir hubungan.
UsingEntity
Sekarang metode dapat digunakan untuk mengonfigurasi ini sebagai jenis entitas gabungan untuk hubungan. Misalnya:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Post>()
.HasMany(e => e.Tags)
.WithMany(e => e.Posts)
.UsingEntity<PostTag>();
}
PostId
dan TagId
secara otomatis diambil sebagai kunci asing dan dikonfigurasi sebagai kunci utama komposit untuk jenis entitas gabungan. Properti yang digunakan untuk kunci asing dapat dikonfigurasi secara eksplisit untuk kasus di mana mereka tidak cocok dengan konvensi EF. Misalnya:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Post>()
.HasMany(e => e.Tags)
.WithMany(e => e.Posts)
.UsingEntity<PostTag>(
l => l.HasOne<Tag>().WithMany().HasForeignKey(e => e.TagId),
r => r.HasOne<Post>().WithMany().HasForeignKey(e => e.PostId));
}
Skema database yang dipetakan untuk tabel gabungan dalam contoh ini secara struktural setara dengan contoh sebelumnya, tetapi dengan beberapa nama kolom yang berbeda:
CREATE TABLE "PostTag" (
"PostId" INTEGER NOT NULL,
"TagId" INTEGER NOT NULL,
CONSTRAINT "PK_PostTag" PRIMARY KEY ("PostId", "TagId"),
CONSTRAINT "FK_PostTag_Posts_PostId" FOREIGN KEY ("PostId") REFERENCES "Posts" ("Id") ON DELETE CASCADE,
CONSTRAINT "FK_PostTag_Tags_TagId" FOREIGN KEY ("TagId") REFERENCES "Tags" ("Id") ON DELETE CASCADE);
Banyak ke banyak dengan navigasi untuk menggabungkan entitas
Mengikuti dari contoh sebelumnya, sekarang ada kelas yang mewakili entitas gabungan, menjadi mudah untuk menambahkan navigasi yang mereferensikan kelas ini. Contohnya:
public class Post
{
public int Id { get; set; }
public List<Tag> Tags { get; } = [];
public List<PostTag> PostTags { get; } = [];
}
public class Tag
{
public int Id { get; set; }
public List<Post> Posts { get; } = [];
public List<PostTag> PostTags { get; } = [];
}
public class PostTag
{
public int PostId { get; set; }
public int TagId { get; set; }
}
Penting
Seperti yang ditunjukkan dalam contoh ini, navigasi ke jenis entitas gabungan dapat digunakan selain navigasi lewati antara dua ujung hubungan banyak ke banyak. Ini berarti bahwa navigasi lewati dapat digunakan untuk berinteraksi dengan hubungan banyak ke banyak secara alami, sementara navigasi ke jenis entitas gabungan dapat digunakan ketika kontrol yang lebih besar atas entitas gabungan itu sendiri diperlukan. Dalam arti tertentu, pemetaan ini memberikan yang terbaik dari kedua dunia antara pemetaan banyak-ke-banyak sederhana, dan pemetaan yang lebih eksplisit cocok dengan skema database.
Tidak ada yang perlu diubah dalam UsingEntity
panggilan, karena navigasi ke entitas gabungan diambil oleh konvensi. Oleh karena itu, konfigurasi untuk contoh ini sama dengan untuk contoh terakhir:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Post>()
.HasMany(e => e.Tags)
.WithMany(e => e.Posts)
.UsingEntity<PostTag>();
}
Navigasi dapat dikonfigurasi secara eksplisit untuk kasus di mana navigasi tidak dapat ditentukan oleh konvensi. Misalnya:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Post>()
.HasMany(e => e.Tags)
.WithMany(e => e.Posts)
.UsingEntity<PostTag>(
l => l.HasOne<Tag>().WithMany(e => e.PostTags),
r => r.HasOne<Post>().WithMany(e => e.PostTags));
}
Skema database yang dipetakan tidak terpengaruh dengan menyertakan navigasi dalam model:
CREATE TABLE "PostTag" (
"PostId" INTEGER NOT NULL,
"TagId" INTEGER NOT NULL,
CONSTRAINT "PK_PostTag" PRIMARY KEY ("PostId", "TagId"),
CONSTRAINT "FK_PostTag_Posts_PostId" FOREIGN KEY ("PostId") REFERENCES "Posts" ("Id") ON DELETE CASCADE,
CONSTRAINT "FK_PostTag_Tags_TagId" FOREIGN KEY ("TagId") REFERENCES "Tags" ("Id") ON DELETE CASCADE);
Banyak ke banyak dengan navigasi ke dan dari entitas gabungan
Contoh sebelumnya menambahkan navigasi ke jenis entitas gabungan dari jenis entitas di salah satu akhir hubungan banyak ke banyak. Navigasi juga dapat ditambahkan ke arah lain, atau di kedua arah. Misalnya:
public class Post
{
public int Id { get; set; }
public List<Tag> Tags { get; } = [];
public List<PostTag> PostTags { get; } = [];
}
public class Tag
{
public int Id { get; set; }
public List<Post> Posts { get; } = [];
public List<PostTag> PostTags { get; } = [];
}
public class PostTag
{
public int PostId { get; set; }
public int TagId { get; set; }
public Post Post { get; set; } = null!;
public Tag Tag { get; set; } = null!;
}
Tidak ada yang perlu diubah dalam UsingEntity
panggilan, karena navigasi ke entitas gabungan diambil oleh konvensi. Oleh karena itu, konfigurasi untuk contoh ini sama dengan untuk contoh terakhir:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Post>()
.HasMany(e => e.Tags)
.WithMany(e => e.Posts)
.UsingEntity<PostTag>();
}
Navigasi dapat dikonfigurasi secara eksplisit untuk kasus di mana navigasi tidak dapat ditentukan oleh konvensi. Misalnya:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Post>()
.HasMany(e => e.Tags)
.WithMany(e => e.Posts)
.UsingEntity<PostTag>(
l => l.HasOne<Tag>(e => e.Tag).WithMany(e => e.PostTags),
r => r.HasOne<Post>(e => e.Post).WithMany(e => e.PostTags));
}
Skema database yang dipetakan tidak terpengaruh dengan menyertakan navigasi dalam model:
CREATE TABLE "PostTag" (
"PostId" INTEGER NOT NULL,
"TagId" INTEGER NOT NULL,
CONSTRAINT "PK_PostTag" PRIMARY KEY ("PostId", "TagId"),
CONSTRAINT "FK_PostTag_Posts_PostId" FOREIGN KEY ("PostId") REFERENCES "Posts" ("Id") ON DELETE CASCADE,
CONSTRAINT "FK_PostTag_Tags_TagId" FOREIGN KEY ("TagId") REFERENCES "Tags" ("Id") ON DELETE CASCADE);
Banyak ke banyak dengan navigasi dan kunci asing yang diubah
Contoh sebelumnya menunjukkan banyak-ke-banyak dengan navigasi ke dan dari jenis entitas gabungan. Contoh ini sama, kecuali bahwa properti kunci asing yang digunakan juga diubah. Misalnya:
public class Post
{
public int Id { get; set; }
public List<Tag> Tags { get; } = [];
public List<PostTag> PostTags { get; } = [];
}
public class Tag
{
public int Id { get; set; }
public List<Post> Posts { get; } = [];
public List<PostTag> PostTags { get; } = [];
}
public class PostTag
{
public int PostForeignKey { get; set; }
public int TagForeignKey { get; set; }
public Post Post { get; set; } = null!;
public Tag Tag { get; set; } = null!;
}
Sekali lagi, UsingEntity
metode ini digunakan untuk mengonfigurasi ini:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Post>()
.HasMany(e => e.Tags)
.WithMany(e => e.Posts)
.UsingEntity<PostTag>(
l => l.HasOne<Tag>(e => e.Tag).WithMany(e => e.PostTags).HasForeignKey(e => e.TagForeignKey),
r => r.HasOne<Post>(e => e.Post).WithMany(e => e.PostTags).HasForeignKey(e => e.PostForeignKey));
}
Skema database yang dipetakan sekarang:
CREATE TABLE "PostTag" (
"PostForeignKey" INTEGER NOT NULL,
"TagForeignKey" INTEGER NOT NULL,
CONSTRAINT "PK_PostTag" PRIMARY KEY ("PostForeignKey", "TagForeignKey"),
CONSTRAINT "FK_PostTag_Posts_PostForeignKey" FOREIGN KEY ("PostForeignKey") REFERENCES "Posts" ("Id") ON DELETE CASCADE,
CONSTRAINT "FK_PostTag_Tags_TagForeignKey" FOREIGN KEY ("TagForeignKey") REFERENCES "Tags" ("Id") ON DELETE CASCADE);
Banyak ke banyak arah unidirectional
Catatan
Hubungan banyak-ke-banyak unidirectional diperkenalkan dalam EF Core 7. Dalam rilis sebelumnya, navigasi privat dapat digunakan sebagai solusinya.
Tidak perlu menyertakan navigasi di kedua sisi hubungan banyak ke banyak. Misalnya:
public class Post
{
public int Id { get; set; }
public List<Tag> Tags { get; } = [];
}
public class Tag
{
public int Id { get; set; }
}
EF perlu beberapa konfigurasi untuk mengetahui bahwa ini harus menjadi hubungan banyak-ke-banyak, bukan satu-ke-banyak. Ini dilakukan menggunakan HasMany
dan WithMany
, tetapi tanpa argumen yang diteruskan di samping tanpa navigasi. Misalnya:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Post>()
.HasMany(e => e.Tags)
.WithMany();
}
Menghapus navigasi tidak memengaruhi skema database:
CREATE TABLE "Posts" (
"Id" INTEGER NOT NULL CONSTRAINT "PK_Posts" PRIMARY KEY AUTOINCREMENT);
CREATE TABLE "Tags" (
"Id" INTEGER NOT NULL CONSTRAINT "PK_Tags" PRIMARY KEY AUTOINCREMENT);
CREATE TABLE "PostTag" (
"PostId" INTEGER NOT NULL,
"TagsId" INTEGER NOT NULL,
CONSTRAINT "PK_PostTag" PRIMARY KEY ("PostId", "TagsId"),
CONSTRAINT "FK_PostTag_Posts_PostId" FOREIGN KEY ("PostId") REFERENCES "Posts" ("Id") ON DELETE CASCADE,
CONSTRAINT "FK_PostTag_Tags_TagsId" FOREIGN KEY ("TagsId") REFERENCES "Tags" ("Id") ON DELETE CASCADE);
Banyak-ke-banyak dan menggabungkan tabel dengan payload
Dalam contoh sejauh ini, tabel gabungan hanya digunakan untuk menyimpan pasangan kunci asing yang mewakili setiap asosiasi. Namun, ini juga dapat digunakan untuk menyimpan informasi tentang asosiasi --misalnya, waktu pembuatannya. Dalam kasus seperti itu, yang terbaik adalah menentukan jenis untuk entitas gabungan dan menambahkan properti "payload asosiasi" ke jenis ini. Juga umum untuk membuat navigasi ke entitas gabungan selain "lewati navigasi" yang digunakan untuk hubungan banyak-ke-banyak. Navigasi tambahan ini memungkinkan entitas gabungan untuk dengan mudah dirujuk dari kode, sehingga memfasilitasi pembacaan dan/atau mengubah data payload. Misalnya:
public class Post
{
public int Id { get; set; }
public List<Tag> Tags { get; } = [];
public List<PostTag> PostTags { get; } = [];
}
public class Tag
{
public int Id { get; set; }
public List<Post> Posts { get; } = [];
public List<PostTag> PostTags { get; } = [];
}
public class PostTag
{
public int PostId { get; set; }
public int TagId { get; set; }
public DateTime CreatedOn { get; set; }
}
Juga umum untuk menggunakan nilai yang dihasilkan untuk properti payload--misalnya, tanda waktu database yang secara otomatis diatur saat baris asosiasi disisipkan. Ini membutuhkan beberapa konfigurasi minimal. Misalnya:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Post>()
.HasMany(e => e.Tags)
.WithMany(e => e.Posts)
.UsingEntity<PostTag>(
j => j.Property(e => e.CreatedOn).HasDefaultValueSql("CURRENT_TIMESTAMP"));
}
Hasil memetakan ke skema jenis entitas dengan tanda waktu yang diatur secara otomatis saat baris disisipkan:
CREATE TABLE "PostTag" (
"PostId" INTEGER NOT NULL,
"TagId" INTEGER NOT NULL,
"CreatedOn" TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP),
CONSTRAINT "PK_PostTag" PRIMARY KEY ("PostId", "TagId"),
CONSTRAINT "FK_PostTag_Posts_PostId" FOREIGN KEY ("PostId") REFERENCES "Posts" ("Id") ON DELETE CASCADE,
CONSTRAINT "FK_PostTag_Tags_TagId" FOREIGN KEY ("TagId") REFERENCES "Tags" ("Id") ON DELETE CASCADE);
Tip
SQL yang ditampilkan di sini adalah untuk SQLite. Di SQL Server/Azure SQL, gunakan .HasDefaultValueSql("GETUTCDATE()")
dan untuk TEXT
membaca datetime
.
Jenis entitas jenis bersama kustom sebagai entitas gabungan
Contoh sebelumnya menggunakan jenis PostTag
sebagai jenis entitas gabungan. Jenis ini khusus untuk hubungan posts-tags. Namun, jika Anda memiliki beberapa tabel gabungan dengan bentuk yang sama, maka jenis CLR yang sama dapat digunakan untuk semuanya. Misalnya, bayangkan bahwa semua tabel gabungan kami memiliki CreatedOn
kolom. Kita dapat memetakan ini menggunakan JoinType
kelas yang dipetakan sebagai jenis entitas jenis bersama:
public class JoinType
{
public int Id1 { get; set; }
public int Id2 { get; set; }
public DateTime CreatedOn { get; set; }
}
Jenis ini kemudian dapat dirujuk sebagai jenis entitas gabungan dengan beberapa hubungan banyak-ke-banyak yang berbeda. Misalnya:
public class Post
{
public int Id { get; set; }
public List<Tag> Tags { get; } = [];
public List<JoinType> PostTags { get; } = [];
}
public class Tag
{
public int Id { get; set; }
public List<Post> Posts { get; } = [];
public List<JoinType> PostTags { get; } = [];
}
public class Blog
{
public int Id { get; set; }
public List<Author> Authors { get; } = [];
public List<JoinType> BlogAuthors { get; } = [];
}
public class Author
{
public int Id { get; set; }
public List<Blog> Blogs { get; } = [];
public List<JoinType> BlogAuthors { get; } = [];
}
Dan hubungan ini kemudian dapat dikonfigurasi dengan tepat untuk memetakan jenis gabungan ke tabel yang berbeda untuk setiap hubungan:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Post>()
.HasMany(e => e.Tags)
.WithMany(e => e.Posts)
.UsingEntity<JoinType>(
"PostTag",
l => l.HasOne<Tag>().WithMany(e => e.PostTags).HasForeignKey(e => e.Id1),
r => r.HasOne<Post>().WithMany(e => e.PostTags).HasForeignKey(e => e.Id2),
j => j.Property(e => e.CreatedOn).HasDefaultValueSql("CURRENT_TIMESTAMP"));
modelBuilder.Entity<Blog>()
.HasMany(e => e.Authors)
.WithMany(e => e.Blogs)
.UsingEntity<JoinType>(
"BlogAuthor",
l => l.HasOne<Author>().WithMany(e => e.BlogAuthors).HasForeignKey(e => e.Id1),
r => r.HasOne<Blog>().WithMany(e => e.BlogAuthors).HasForeignKey(e => e.Id2),
j => j.Property(e => e.CreatedOn).HasDefaultValueSql("CURRENT_TIMESTAMP"));
}
Ini menghasilkan tabel berikut dalam skema database:
CREATE TABLE "BlogAuthor" (
"Id1" INTEGER NOT NULL,
"Id2" INTEGER NOT NULL,
"CreatedOn" TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP),
CONSTRAINT "PK_BlogAuthor" PRIMARY KEY ("Id1", "Id2"),
CONSTRAINT "FK_BlogAuthor_Authors_Id1" FOREIGN KEY ("Id1") REFERENCES "Authors" ("Id") ON DELETE CASCADE,
CONSTRAINT "FK_BlogAuthor_Blogs_Id2" FOREIGN KEY ("Id2") REFERENCES "Blogs" ("Id") ON DELETE CASCADE);
CREATE TABLE "PostTag" (
"Id1" INTEGER NOT NULL,
"Id2" INTEGER NOT NULL,
"CreatedOn" TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP),
CONSTRAINT "PK_PostTag" PRIMARY KEY ("Id1", "Id2"),
CONSTRAINT "FK_PostTag_Posts_Id2" FOREIGN KEY ("Id2") REFERENCES "Posts" ("Id") ON DELETE CASCADE,
CONSTRAINT "FK_PostTag_Tags_Id1" FOREIGN KEY ("Id1") REFERENCES "Tags" ("Id") ON DELETE CASCADE);
Banyak ke banyak dengan kunci alternatif
Sejauh ini, semua contoh telah menunjukkan kunci asing dalam jenis entitas gabungan yang dibatasi ke kunci utama jenis entitas di kedua sisi hubungan. Setiap kunci asing, atau keduanya, dapat dibatasi ke kunci alternatif. Misalnya, pertimbangkan model ini di manaTag
dan Post
memiliki properti kunci alternatif:
public class Post
{
public int Id { get; set; }
public int AlternateKey { get; set; }
public List<Tag> Tags { get; } = [];
}
public class Tag
{
public int Id { get; set; }
public int AlternateKey { get; set; }
public List<Post> Posts { get; } = [];
}
Konfigurasi untuk model ini adalah:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Post>()
.HasMany(e => e.Tags)
.WithMany(e => e.Posts)
.UsingEntity(
l => l.HasOne(typeof(Tag)).WithMany().HasPrincipalKey(nameof(Tag.AlternateKey)),
r => r.HasOne(typeof(Post)).WithMany().HasPrincipalKey(nameof(Post.AlternateKey)));
}
Dan skema database yang dihasilkan, untuk kejelasan, termasuk juga tabel dengan kunci alternatif:
CREATE TABLE "Posts" (
"Id" INTEGER NOT NULL CONSTRAINT "PK_Posts" PRIMARY KEY AUTOINCREMENT,
"AlternateKey" INTEGER NOT NULL,
CONSTRAINT "AK_Posts_AlternateKey" UNIQUE ("AlternateKey"));
CREATE TABLE "Tags" (
"Id" INTEGER NOT NULL CONSTRAINT "PK_Tags" PRIMARY KEY AUTOINCREMENT,
"AlternateKey" INTEGER NOT NULL,
CONSTRAINT "AK_Tags_AlternateKey" UNIQUE ("AlternateKey"));
CREATE TABLE "PostTag" (
"PostsAlternateKey" INTEGER NOT NULL,
"TagsAlternateKey" INTEGER NOT NULL,
CONSTRAINT "PK_PostTag" PRIMARY KEY ("PostsAlternateKey", "TagsAlternateKey"),
CONSTRAINT "FK_PostTag_Posts_PostsAlternateKey" FOREIGN KEY ("PostsAlternateKey") REFERENCES "Posts" ("AlternateKey") ON DELETE CASCADE,
CONSTRAINT "FK_PostTag_Tags_TagsAlternateKey" FOREIGN KEY ("TagsAlternateKey") REFERENCES "Tags" ("AlternateKey") ON DELETE CASCADE);
Konfigurasi untuk menggunakan kunci alternatif sedikit berbeda jika jenis entitas gabungan diwakili oleh jenis .NET. Misalnya:
public class Post
{
public int Id { get; set; }
public int AlternateKey { get; set; }
public List<Tag> Tags { get; } = [];
public List<PostTag> PostTags { get; } = [];
}
public class Tag
{
public int Id { get; set; }
public int AlternateKey { get; set; }
public List<Post> Posts { get; } = [];
public List<PostTag> PostTags { get; } = [];
}
public class PostTag
{
public int PostId { get; set; }
public int TagId { get; set; }
public Post Post { get; set; } = null!;
public Tag Tag { get; set; } = null!;
}
Konfigurasi sekarang dapat menggunakan metode generik UsingEntity<>
:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Post>()
.HasMany(e => e.Tags)
.WithMany(e => e.Posts)
.UsingEntity<PostTag>(
l => l.HasOne<Tag>(e => e.Tag).WithMany(e => e.PostTags).HasPrincipalKey(e => e.AlternateKey),
r => r.HasOne<Post>(e => e.Post).WithMany(e => e.PostTags).HasPrincipalKey(e => e.AlternateKey));
}
Dan skema yang dihasilkan adalah:
CREATE TABLE "Posts" (
"Id" INTEGER NOT NULL CONSTRAINT "PK_Posts" PRIMARY KEY AUTOINCREMENT,
"AlternateKey" INTEGER NOT NULL,
CONSTRAINT "AK_Posts_AlternateKey" UNIQUE ("AlternateKey"));
CREATE TABLE "Tags" (
"Id" INTEGER NOT NULL CONSTRAINT "PK_Tags" PRIMARY KEY AUTOINCREMENT,
"AlternateKey" INTEGER NOT NULL,
CONSTRAINT "AK_Tags_AlternateKey" UNIQUE ("AlternateKey"));
CREATE TABLE "PostTag" (
"PostId" INTEGER NOT NULL,
"TagId" INTEGER NOT NULL,
CONSTRAINT "PK_PostTag" PRIMARY KEY ("PostId", "TagId"),
CONSTRAINT "FK_PostTag_Posts_PostId" FOREIGN KEY ("PostId") REFERENCES "Posts" ("AlternateKey") ON DELETE CASCADE,
CONSTRAINT "FK_PostTag_Tags_TagId" FOREIGN KEY ("TagId") REFERENCES "Tags" ("AlternateKey") ON DELETE CASCADE);
Banyak-ke-banyak dan menggabungkan tabel dengan kunci primer terpisah
Sejauh ini, jenis entitas gabungan dalam semua contoh memiliki kunci primer yang terdiri dari dua properti kunci asing. Ini karena setiap kombinasi nilai untuk properti ini dapat terjadi paling banyak sekali. Oleh karena itu, properti ini membentuk kunci primer alami.
Catatan
EF Core tidak mendukung entitas duplikat dalam navigasi koleksi apa pun.
Jika Anda mengontrol skema database, maka tidak ada alasan bagi tabel gabungan untuk memiliki kolom kunci primer tambahan, Namun, ada kemungkinan bahwa tabel gabungan yang ada mungkin memiliki kolom kunci utama yang ditentukan. EF masih dapat memetakan ini dengan beberapa konfigurasi.
Mungkin paling mudah untuk ini dengan membuat kelas untuk mewakili entitas gabungan. Misalnya:
public class Post
{
public int Id { get; set; }
public List<Tag> Tags { get; } = [];
}
public class Tag
{
public int Id { get; set; }
public List<Post> Posts { get; } = [];
}
public class PostTag
{
public int Id { get; set; }
public int PostId { get; set; }
public int TagId { get; set; }
}
Properti ini PostTag.Id
sekarang diambil sebagai kunci utama menurut konvensi, sehingga satu-satunya konfigurasi yang diperlukan adalah panggilan ke UsingEntity
untuk jenis tersebut PostTag
:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Post>()
.HasMany(e => e.Tags)
.WithMany(e => e.Posts)
.UsingEntity<PostTag>();
}
Dan skema yang dihasilkan untuk tabel gabungan adalah:
CREATE TABLE "PostTag" (
"Id" INTEGER NOT NULL CONSTRAINT "PK_PostTag" PRIMARY KEY AUTOINCREMENT,
"PostId" INTEGER NOT NULL,
"TagId" INTEGER NOT NULL,
CONSTRAINT "FK_PostTag_Posts_PostId" FOREIGN KEY ("PostId") REFERENCES "Posts" ("Id") ON DELETE CASCADE,
CONSTRAINT "FK_PostTag_Tags_TagId" FOREIGN KEY ("TagId") REFERENCES "Tags" ("Id") ON DELETE CASCADE);
Kunci primer juga dapat ditambahkan ke entitas gabungan tanpa menentukan kelas untuk itu. Misalnya, dengan jenis dan Tag
sajaPost
:
public class Post
{
public int Id { get; set; }
public List<Tag> Tags { get; } = [];
}
public class Tag
{
public int Id { get; set; }
public List<Post> Posts { get; } = [];
}
Kunci dapat ditambahkan dengan konfigurasi ini:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Post>()
.HasMany(e => e.Tags)
.WithMany(e => e.Posts)
.UsingEntity(
j =>
{
j.IndexerProperty<int>("Id");
j.HasKey("Id");
});
}
Yang menghasilkan tabel gabungan dengan kolom kunci utama terpisah:
CREATE TABLE "PostTag" (
"Id" INTEGER NOT NULL CONSTRAINT "PK_PostTag" PRIMARY KEY AUTOINCREMENT,
"PostsId" INTEGER NOT NULL,
"TagsId" INTEGER NOT NULL,
CONSTRAINT "FK_PostTag_Posts_PostsId" FOREIGN KEY ("PostsId") REFERENCES "Posts" ("Id") ON DELETE CASCADE,
CONSTRAINT "FK_PostTag_Tags_TagsId" FOREIGN KEY ("TagsId") REFERENCES "Tags" ("Id") ON DELETE CASCADE);
Banyak ke banyak tanpa penghapusan berkaskala
Dalam semua contoh yang ditunjukkan di atas, kunci asing yang dibuat antara tabel gabungan dan dua sisi hubungan banyak-ke-banyak dibuat dengan perilaku penghapusan bertingkat. Ini sangat berguna karena berarti bahwa jika entitas di kedua sisi hubungan dihapus, maka baris dalam tabel gabungan untuk entitas tersebut akan dihapus secara otomatis. Atau, dengan kata lain, ketika entitas tidak ada lagi, maka hubungannya dengan entitas lain juga tidak ada lagi.
Sulit untuk membayangkan kapan berguna untuk mengubah perilaku ini, tetapi dapat dilakukan jika diinginkan. Misalnya:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Post>()
.HasMany(e => e.Tags)
.WithMany(e => e.Posts)
.UsingEntity(
l => l.HasOne(typeof(Tag)).WithMany().OnDelete(DeleteBehavior.Restrict),
r => r.HasOne(typeof(Post)).WithMany().OnDelete(DeleteBehavior.Restrict));
}
Skema database untuk tabel gabungan menggunakan perilaku penghapusan terbatas pada batasan kunci asing:
CREATE TABLE "PostTag" (
"PostsId" INTEGER NOT NULL,
"TagsId" INTEGER NOT NULL,
CONSTRAINT "PK_PostTag" PRIMARY KEY ("PostsId", "TagsId"),
CONSTRAINT "FK_PostTag_Posts_PostsId" FOREIGN KEY ("PostsId") REFERENCES "Posts" ("Id") ON DELETE RESTRICT,
CONSTRAINT "FK_PostTag_Tags_TagsId" FOREIGN KEY ("TagsId") REFERENCES "Tags" ("Id") ON DELETE RESTRICT);
Mereferensikan sendiri banyak-ke-banyak
Jenis entitas yang sama dapat digunakan di kedua ujung hubungan banyak ke banyak; ini dikenal sebagai hubungan "referensi diri". Misalnya:
public class Person
{
public int Id { get; set; }
public List<Person> Parents { get; } = [];
public List<Person> Children { get; } = [];
}
Ini memetakan ke tabel gabungan yang disebut PersonPerson
, dengan kedua kunci asing menunjuk kembali ke People
tabel:
CREATE TABLE "PersonPerson" (
"ChildrenId" INTEGER NOT NULL,
"ParentsId" INTEGER NOT NULL,
CONSTRAINT "PK_PersonPerson" PRIMARY KEY ("ChildrenId", "ParentsId"),
CONSTRAINT "FK_PersonPerson_People_ChildrenId" FOREIGN KEY ("ChildrenId") REFERENCES "People" ("Id") ON DELETE CASCADE,
CONSTRAINT "FK_PersonPerson_People_ParentsId" FOREIGN KEY ("ParentsId") REFERENCES "People" ("Id") ON DELETE CASCADE);
Referensi mandiri simetris banyak-ke-banyak
Terkadang hubungan banyak ke banyak secara alami simetris. Artinya, jika entitas A terkait dengan entitas B, maka entitas B juga terkait dengan entitas A. Ini secara alami dimodelkan menggunakan satu navigasi. Misalnya, bayangkan kasus di mana adalah orang A adalah teman dengan orang B, maka orang B berteman dengan orang A:
public class Person
{
public int Id { get; set; }
public List<Person> Friends { get; } = [];
}
Sayangnya, ini tidak mudah untuk dipetakan. Navigasi yang sama tidak dapat digunakan untuk kedua akhir hubungan. Yang terbaik yang dapat dilakukan adalah memetakannya sebagai hubungan banyak-ke-banyak yang tidak langsung. Misalnya:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Person>()
.HasMany(e => e.Friends)
.WithMany();
}
Namun, untuk memastikan dua orang keduanya terkait satu sama lain, setiap orang harus ditambahkan secara manual ke koleksi orang Friends
lain. Misalnya:
ginny.Friends.Add(hermione);
hermione.Friends.Add(ginny);
Penggunaan langsung tabel gabungan
Semua contoh di atas menggunakan pola pemetaan EF Core banyak ke banyak. Namun, dimungkinkan juga untuk memetakan tabel gabungan ke jenis entitas normal dan hanya menggunakan dua hubungan satu-ke-banyak untuk semua operasi.
Misalnya, jenis entitas ini mewakili pemetaan dua tabel normal dan tabel gabungan tanpa menggunakan hubungan banyak-ke-banyak:
public class Post
{
public int Id { get; set; }
public List<PostTag> PostTags { get; } = new();
}
public class Tag
{
public int Id { get; set; }
public List<PostTag> PostTags { get; } = new();
}
public class PostTag
{
public int PostId { get; set; }
public int TagId { get; set; }
public Post Post { get; set; } = null!;
public Tag Tag { get; set; } = null!;
}
Ini tidak memerlukan pemetaan khusus, karena ini adalah jenis entitas normal dengan hubungan satu-ke-banyak normal.
Sumber daya tambahan
- Sesi Standup Komunitas Data .NET, dengan mendalami banyak ke banyak dan infrastruktur yang mendukungnya.