Relacje w kontekście bazy danych definiują, w jaki sposób dwie encje są ze sobą powiązane. Entity Framework Core naprawdę błyszczy w sposobie w jaki wspiera relacje. Oferuje konfigurację opartą na konwencji, która skonfiguruje relacje w oparciu o dostarczony model. W bardziej zaawansowanych przypadkach możemy skorzystać z solidnych możliwości Fluent API, które zapewnia większą elastyczność.
Muszę przyznać, że praca z relacjami w Entity Framework Core 5 jest dla programisty bardzo naturalna i być może to właśnie jest jego najważniejsza cecha.
Typy relacji
Relacje w bazie danych będą oznaczać, że dwie encje są ze sobą powiązane. Są logicznie połączone. Spójrzmy na przykład model hotelu:
Mamy rezerwację, która dotyczy jednego pokoju i zawiera listę gości. Do pokoju można przypisać wiele rezerwacji. Profil również można przypisać do wielu rezerwacji, ale jest powiązany tylko z jednym adresem. Mamy tu zdefiniowane 3 różne typy relacji:
- jeden-do-wielu – pokój do rezerwacji
- wiele-do-wielu – rezerwacja i profil
- jeden-do-jednego – profil i adres
Te typy są dobrze obsługiwane przez Entity Framework Core, więc przyjrzyjmy się modelowi, który odpowiada temu schematowi. Oto Reservation
:
public class Reservation { public int Id { get; set; } public int RoomId { get; set; } public Room Room { get; set; } public List<Profile> Profiles { get; set; } public DateTime Created { get; set; } public DateTime From { get; set; } public DateTime To { get; set; } }
Oraz Room
:
public class Room { public int Id { get; set; } public int Number { get; set; } public string Description { get; set; } public DateTime LastBooked { get; set; } public int Level { get; set; } public RoomType RoomType { get; set; } public bool WithBathroom { get; set; } public int NumberOfPlacesToSleep { get; set; } } public enum RoomType { Standard, Suite }
Oraz Profile
:
public class Profile { public int Id { get; set; } public string Ref { get; set; } public string Salutation { get; set; } public string Forename { get; set; } public string Surname { get; set; } public string TelNo { get; set; } public string Email { get; set; } public string Country { get; set; } public DateTime? DateOfBirth { get; set; } public Address Address { get; set; } public List<Reservation> Reservations { get; set; } }
I w końcu Address
:
public class Address { public int Id { get; set; } public string Street { get; set; } public string HouseNumber { get; set; } public string City { get; set; } public string PostCode { get; set; } public int ProfileId { get; set; } public Profile Profile { get; set; } }
I wisienka na torcie, czyli PrimeDbContext
public class PrimeDbContext : DbContext { public PrimeDbContext(DbContextOptions<PrimeDbContext> options) : base(options) { } public virtual DbSet<Room> Rooms { get; set; } public virtual DbSet<Profile> Profiles { get; set; } public virtual DbSet<Reservation> Reservations { get; set; } public virtual DbSet<Address> Address { get; set; } protected override void OnModelCreating(ModelBuilder modelBuilder) { } }
Proszę zwróć uwagę na bardzo ważną rzecz, dzięki konfiguracji opartej na konwencji nie jest wymagana dodatkowa konfiguracja w klasach modelu i klasie PrimeDbContext
.
Konfiguracja jest prosta
Czy zauważyłeś, jak łatwo jest skonfigurować relacje w Entity Framework Core 5? Jeśli masz właściwości poprawnie nazwane, EF Core samodzielnie rozpozna twoje relacje. Relacja jest definiowana przez właściwość nawigacyjne, czyli encję wewnątrz innej encji. Spójrz na rezerwację. Istnieje Room
, czyli właściwość nawigacyjna, oraz RoomId
, który będzie traktowany jako klucz obcy w celu zdefiniowania powiązania.
Istnieją 3 sposoby konfigurowania modelu i relacji:
- oparta na konwencji – z odpowiednio nazwanymi właściwościami EF Core rozlizna jak jednostki są powiązane
- data annotations – przydatne atrybuty, które można umieścić we nad właściwością encji
- Fluent API – w pełni funkcjonalne API do konfigurowania relacji i encji według własnego uznania
Oto przykład data annotations, aby ustawić niestandardowo nazwany klucz obcy:
public class Post { public int PostId { get; set; } public string Title { get; set; } public string Content { get; set; } public int BlogForeignKey { get; set; } [ForeignKey("BlogForeignKey")] public Blog Blog { get; set; } }
Jeśli klucz obcy miałby nazwę BlogId
, to zostałby skonfigurowany automatycznie, ale nazwa niestandardowa musi być obsłużona ręcznie.
Nawet w przypadku relacji wiele-do-wielu nie ma potrzeby definiowania tabeli łączącej i pisania dodatkowej konfiguracji. Jest to nowa funkcja dostępna od wersji RC1, więc dokumentacja dostępna na oficjalnej stronie Microsoft może wprowadzać w błąd. Miejmy nadzieję, że wkrótce zostanie zaktualizowana.
Na szczęście w większości przypadków nie musisz ręcznie pisać dużej części konfiguracji, ponieważ jest to konieczne głównie podczas radzenia sobie z zaawansowanymi scenariuszami i niestandardowymi mapowaniami.
Podejście Model First
Podejście Model First umożliwia definiowanie modelu i relacji oraz używanie Entity Framework Core do generowania kodu SQL. Wszystko, co musisz zrobić, to stworzyć modele, które chcesz, a kiedy skończysz, po prostu utwórz migrację bazy danych. Przy założeniu, że masz już migracje EF Core.
Wspomniane podejście działa również w przypadku aktualizowania modelu, gdy trzeba dodać powiązaną encję, migracje EF Core poradzą sobie z tym zaskakująco dobrze.
Powiedzmy, że mam encję Profile
i chcemy dodać encję Address
w relacji jeden-do-jednego. Możesz spojrzeć na kod obu tych klas powyżej. Po dodaniu nowej migracji za pomocą dotnet CLI otrzymuję nową migrację, już wygenerowaną, na podstawie zmian mojego modelu.
public partial class AddAddress : Migration { protected override void Up(MigrationBuilder migrationBuilder) { migrationBuilder.CreateTable( name: "Address", columns: table => new { Id = table.Column<int>(type: "int", nullable: false) .Annotation("SqlServer:Identity", "1, 1"), Street = table.Column<string>(type: "nvarchar(max)", nullable: true), HouseNumber = table.Column<string>(type: "nvarchar(max)", nullable: true), City = table.Column<string>(type: "nvarchar(max)", nullable: true), PostCode = table.Column<string>(type: "nvarchar(max)", nullable: true), ProfileId = table.Column<int>(type: "int", nullable: false) }, constraints: table => { table.PrimaryKey("PK_Address", x => x.Id); table.ForeignKey( name: "FK_Address_Profiles_ProfileId", column: x => x.ProfileId, principalTable: "Profiles", principalColumn: "Id", onDelete: ReferentialAction.Cascade); }); migrationBuilder.CreateIndex( name: "IX_Address_ProfileId", table: "Address", column: "ProfileId", unique: true); } protected override void Down(MigrationBuilder migrationBuilder) { migrationBuilder.DropTable( name: "Address"); } }
Łatwo i przyjemnie, ale co najważniejsze – działa!
Podsumowanie
Konfigurowanie relacji w Entity Framework Core 5 jest tak proste, jak to tylko możliwe. Większość mapowania może być wykonana automatycznie przez framework, po prostu przez odpowiednie nazwanie właściwości. Jeśli zmagasz się z bardziej zaawansowanymi scenariuszami, możesz skorzystać z Fluent API, które oferuje dużo i a jego zapis jest dość zwięzły. W zaledwie kilku wierszach można zdefiniować na przykład mapowanie widoku na encję.
Moją ulubioną częścią jest jednak podejście Model First, w którym tworzysz model, z którym chcesz pracować, i generujesz kod SQL z migracjami EF Core.
Cały wymieniony tutaj kod jest dostępny na moim GitHubie, kodzie używającym również tych relacji! Zapraszam do odwiedzenia.