Monthly Archives: October 2020

Jak stworzyć relacje w Entity Framework Core 5

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.

How to configure relationships in Entity Framework Core 5

Relationships in a database context define how two entities relate to each other. Entity Framework Core really shines in supporting relationships. It offers a convention-based configuration, that will configure relationships based on the model provided. For more advanced cases we can use robust Fluent API capabilities, that offer greater flexibility.

I must admit that working with relationships in Entity Framework Core 5 feels very natural for a developer and that might be simply its most important feature.

Types of relationships

Relationships in the database will mean, that two entities relate to each other. They are logically connected. Let’s take a look at a hotel model example:

We have a reservation, that has a single room and a list of guests. The room can be assigned to many reservations. A profile can be assigned to many reservations, but it has only one address related. We have 3 different types of relationships defined here:

  • one-to-many – room to a reservation
  • many-to-many – reservation to profile
  • one-to-one – profile to address

Those types are nicely supported by Entity Framework Core, so let’s have a look at the model that corresponds to this schema. Here is a 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; }
    }

And 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
    }

And 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; }
    }

And finally, 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; }
    }

And a cherry on top, a 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)
        {
        }
    }

Please notice a very important thing, thanks to a convention-based configuration, there is no additional configuration needed in the model classes and PrimeDbContext class.

Configuration is easy

Have you noticed how easy it is to configure relationships in Entity Framework Core 5? If you have properties named correctly, then EF Core will deduct relationships on its own. A relationship is defined by a navigation property, that is an entity inside an entity. Look at the Reservation. There is a Room, that is a navigation property and there is a RoomId that will be treated as a foreign key to define a constraint. 

There are 3 ways to configure model and relationships:

  • convention-based – with properly named properties, EF Core will deduct how entities are related
  • data annotations – handy attributes that you can put on the entity property
  • Fluent API – a fully-featured API for configuring relations and entities as you wish

Here is an example of a data annotation to set a non-standard named foreign key:

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; }
}

If the foreign key would be named BlogId, then it would be configured automatically, but a custom name has to be handled manually.

Even with many-to-many relationships there is no need to define a joining table and write additional configuration. It is a new feature available from RC1 version, so documentation available on the official Microsoft page can be misleading. Hopefully, it will be updated soon.

Luckily, in most cases, you would not need to write much of configuration manually, cause it is mainly required when coping with advanced scenarios and custom mappings.

Model First approach

Model First approach lets you define your model and relationships and use Entity Framework Core to generate SQL for you. All you need to do is to create models that you want and when you’re done, just create a database migration. This is true of course when you already have EF Core migrations in place. 

It also works for updating the model, when you need to add a related entity, EF Core migrations will handle that surprisingly well.

Let’s say I had a profile entity and I wanted to add an Address entity in a one-to-one relationship. You can take a look at the code of those both classes above. When I add a new migration from dotnet CLI, I get a new migration like this, already generated for me, based on my model changes.

    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");
        }
    }

Nice and easy, but what’s most important – it works!

Summary

Configuring relationships in Entity Framework Core 5 is as simple as it can possibly be. Most of the mapping can be done automatically by the framework, just by naming properties right. If you’re struggling with more advanced scenarios you can go with Fluent API, which offers a lot and is rather compact. In just a few lines you can define how to map a view to an entity.

My favorite part, though, is a Model First approach, where you create a model that you would like to work with and generate SQL with EF Core migrations. 

All code mentioned here is available on my GitHub, code that uses those relationships as well! Feel free to drop by.