Co nowego w programie EF Core 7.0

Program EF Core 7.0 (EF7) został wydany w listopadzie 2022 r.

Napiwek

Przykładowe przykłady można uruchamiać i debugować , pobierając przykładowy kod z usługi GitHub. Każda sekcja łączy się z kodem źródłowym specyficznym dla tej sekcji.

Program EF7 jest przeznaczony dla platformy .NET 6 i może być używany z platformą .NET 6 (LTS) lub .NET 7.

Przykładowy model

Wiele z poniższych przykładów używa prostego modelu z blogami, wpisami, tagami i autorami:

public class Blog
{
    public Blog(string name)
    {
        Name = name;
    }

    public int Id { get; private set; }
    public string Name { get; set; }
    public List<Post> Posts { get; } = new();
}

public class Post
{
    public Post(string title, string content, DateTime publishedOn)
    {
        Title = title;
        Content = content;
        PublishedOn = publishedOn;
    }

    public int Id { get; private set; }
    public string Title { get; set; }
    public string Content { get; set; }
    public DateTime PublishedOn { get; set; }
    public Blog Blog { get; set; } = null!;
    public List<Tag> Tags { get; } = new();
    public Author? Author { get; set; }
    public PostMetadata? Metadata { get; set; }
}

public class FeaturedPost : Post
{
    public FeaturedPost(string title, string content, DateTime publishedOn, string promoText)
        : base(title, content, publishedOn)
    {
        PromoText = promoText;
    }

    public string PromoText { get; set; }
}

public class Tag
{
    public Tag(string id, string text)
    {
        Id = id;
        Text = text;
    }

    public string Id { get; private set; }
    public string Text { get; set; }
    public List<Post> Posts { get; } = new();
}

public class Author
{
    public Author(string name)
    {
        Name = name;
    }

    public int Id { get; private set; }
    public string Name { get; set; }
    public ContactDetails Contact { get; set; } = null!;
    public List<Post> Posts { get; } = new();
}

Niektóre z przykładów używają również typów agregacji, które są mapowane na różne sposoby w różnych próbkach. Istnieje jeden typ agregacji dla kontaktów:

public class ContactDetails
{
    public Address Address { get; set; } = null!;
    public string? Phone { get; set; }
}

public class Address
{
    public Address(string street, string city, string postcode, string country)
    {
        Street = street;
        City = city;
        Postcode = postcode;
        Country = country;
    }

    public string Street { get; set; }
    public string City { get; set; }
    public string Postcode { get; set; }
    public string Country { get; set; }
}

Drugi typ agregacji dla metadanych post:

public class PostMetadata
{
    public PostMetadata(int views)
    {
        Views = views;
    }

    public int Views { get; set; }
    public List<SearchTerm> TopSearches { get; } = new();
    public List<Visits> TopGeographies { get; } = new();
    public List<PostUpdate> Updates { get; } = new();
}

public class SearchTerm
{
    public SearchTerm(string term, int count)
    {
        Term = term;
        Count = count;
    }

    public string Term { get; private set; }
    public int Count { get; private set; }
}

public class Visits
{
    public Visits(double latitude, double longitude, int count)
    {
        Latitude = latitude;
        Longitude = longitude;
        Count = count;
    }

    public double Latitude { get; private set; }
    public double Longitude { get; private set; }
    public int Count { get; private set; }
    public List<string>? Browsers { get; set; }
}

public class PostUpdate
{
    public PostUpdate(IPAddress postedFrom, DateTime updatedOn)
    {
        PostedFrom = postedFrom;
        UpdatedOn = updatedOn;
    }

    public IPAddress PostedFrom { get; private set; }
    public string? UpdatedBy { get; init; }
    public DateTime UpdatedOn { get; private set; }
    public List<Commit> Commits { get; } = new();
}

public class Commit
{
    public Commit(DateTime committedOn, string comment)
    {
        CommittedOn = committedOn;
        Comment = comment;
    }

    public DateTime CommittedOn { get; private set; }
    public string Comment { get; set; }
}

Napiwek

Przykładowy model można znaleźć w pliku BlogsContext.cs.

Kolumny JSON

Większość relacyjnych baz danych obsługuje kolumny zawierające dokumenty JSON. Dane JSON w tych kolumnach można przechodzić do szczegółów za pomocą zapytań. Umożliwia to na przykład filtrowanie i sortowanie według elementów dokumentów, a także projekcję elementów z dokumentów w wynikach. Kolumny JSON umożliwiają relacyjnym bazom danych przejmowanie niektórych cech baz danych dokumentów, tworząc przydatną hybrydę między nimi.

Program EF7 zawiera niezależną od dostawcy obsługę kolumn JSON z implementacją programu SQL Server. Ta obsługa umożliwia mapowanie agregacji utworzonych na podstawie typów platformy .NET do dokumentów JSON. Normalne zapytania LINQ mogą być używane w agregacjach i zostaną one przetłumaczone na odpowiednie konstrukcje zapytań potrzebne do przechodzenia do szczegółów w formacie JSON. Program EF7 obsługuje również aktualizowanie i zapisywanie zmian w dokumentach JSON.

Uwaga

Obsługa formatu JSON w formacie SQLite jest planowana na post EF7. Dostawcy PostgreSQL i Pomelo MySQL już obsługują kolumny JSON. Będziemy współpracować z autorami tych dostawców w celu dostosowania obsługi kodu JSON we wszystkich dostawcach.

Mapowanie na kolumny JSON

W programie EF Core typy agregacji są definiowane przy użyciu elementów OwnsOne i OwnsMany. Rozważmy na przykład typ agregacji z naszego przykładowego modelu używanego do przechowywania informacji kontaktowych:

public class ContactDetails
{
    public Address Address { get; set; } = null!;
    public string? Phone { get; set; }
}

public class Address
{
    public Address(string street, string city, string postcode, string country)
    {
        Street = street;
        City = city;
        Postcode = postcode;
        Country = country;
    }

    public string Street { get; set; }
    public string City { get; set; }
    public string Postcode { get; set; }
    public string Country { get; set; }
}

Można go następnie użyć w typie jednostki "właściciel", na przykład do przechowywania danych kontaktowych autora:

public class Author
{
    public int Id { get; set; }
    public string Name { get; set; }
    public ContactDetails Contact { get; set; }
}

Typ agregacji jest skonfigurowany przy OnModelCreating użyciu polecenia OwnsOne:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Author>().OwnsOne(
        author => author.Contact, ownedNavigationBuilder =>
        {
            ownedNavigationBuilder.OwnsOne(contactDetails => contactDetails.Address);
        });
}

Napiwek

Pokazany tutaj kod pochodzi z pliku JsonColumnsSample.cs.

Domyślnie dostawcy relacyjnej bazy danych mapują typy agregacji, takie jak ta, do tej samej tabeli co typ jednostki będąca właścicielem. Oznacza to, że każda właściwość ContactDetails klas i Address jest mapowana na kolumnę w Authors tabeli.

Niektórzy zapisani autorzy z danymi kontaktowymi będą wyglądać następująco:

Autorów

Id Nazwisko Contact_Address_Street Contact_Address_City Contact_Address_Postcode Contact_Address_Country Contact_Telefon
1 Maddy Montaquila 1 Główny St Camberwick Zielony CW1 5ZH Zjednoczone Królestwo 01632 12345
2 Jeremy Likness 2 St Chigley CW1 5ZH Zjednoczone Królestwo 01632 12346
3 Daniel Roth 3 St Camberwick Zielony CW1 5ZH Zjednoczone Królestwo 01632 12347
4 Arthur Vickers 15a Main St Chigley CW1 5ZH Zjednoczone Królestwo 01632 22345
5 Brice Lambson 4 Main St Chigley CW1 5ZH Zjednoczone Królestwo 01632 12349

W razie potrzeby każdy typ jednostki tworzący agregację można zamapować na własną tabelę:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Author>().OwnsOne(
        author => author.Contact, ownedNavigationBuilder =>
        {
            ownedNavigationBuilder.ToTable("Contacts");
            ownedNavigationBuilder.OwnsOne(
                contactDetails => contactDetails.Address, ownedOwnedNavigationBuilder =>
                {
                    ownedOwnedNavigationBuilder.ToTable("Addresses");
                });
        });
}

Te same dane są następnie przechowywane w trzech tabelach:

Autorów

Id Nazwisko
1 Maddy Montaquila
2 Jeremy Likness
3 Daniel Roth
4 Arthur Vickers
5 Brice Lambson

Kontakty

Identyfikator autora Telefonu
1 01632 12345
2 01632 12346
3 01632 12347
4 01632 22345
5 01632 12349

Adresy

ContactDetailsAuthorId Ulica City Kodów pocztowych Kraj
1 1 Główny St Camberwick Zielony CW1 5ZH Zjednoczone Królestwo
2 2 St Chigley CW1 5ZH Zjednoczone Królestwo
3 3 St Camberwick Zielony CW1 5ZH Zjednoczone Królestwo
4 15a Main St Chigley CW1 5ZH Zjednoczone Królestwo
5 4 Main St Chigley CW1 5ZH Zjednoczone Królestwo

Teraz, w interesującej części. W programie EF7 ContactDetails typ agregacji można zamapować na kolumnę JSON. Wymaga to tylko jednego wywołania ToJson() podczas konfigurowania typu agregacji:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Author>().OwnsOne(
        author => author.Contact, ownedNavigationBuilder =>
        {
            ownedNavigationBuilder.ToJson();
            ownedNavigationBuilder.OwnsOne(contactDetails => contactDetails.Address);
        });
}

Tabela Authors będzie teraz zawierać kolumnę JSON wypełniona ContactDetails dokumentem JSON dla każdego autora:

Autorów

Id Nazwisko Kontakt
1 Maddy Montaquila {
  "Telefon":"01632 12345",
  "Adres": {
    "City":"Camberwick Green",
    "Country":"UK",
    "Kod pocztowy":"CW1 5ZH",
    "Street":"1 Main St"
  }
}
2 Jeremy Likness {
  "Telefon":"01632 12346",
  "Adres": {
    "Miasto":"Chigley",
    "Country":"UK",
    "Kod pocztowy":"CH1 5ZH",
    "Street":"2 Main St"
  }
}
3 Daniel Roth {
  "Telefon":"01632 12347",
  "Adres": {
    "City":"Camberwick Green",
    "Country":"UK",
    "Kod pocztowy":"CW1 5ZH",
    "Street":"3 Main St"
  }
}
4 Arthur Vickers {
  "Telefon":"01632 12348",
  "Adres": {
    "Miasto":"Chigley",
    "Country":"UK",
    "Kod pocztowy":"CH1 5ZH",
    "Street":"15a Main St"
  }
}
5 Brice Lambson {
  "Telefon":"01632 12349",
  "Adres": {
    "Miasto":"Chigley",
    "Country":"UK",
    "Kod pocztowy":"CH1 5ZH",
    "Street":"4 Main St"
  }
}

Napiwek

To użycie agregacji jest bardzo podobne do sposobu mapowania dokumentów JSON podczas korzystania z dostawcy EF Core dla usługi Azure Cosmos DB. Kolumny JSON umożliwiają używanie programu EF Core względem baz danych dokumentów do dokumentów osadzonych w relacyjnej bazie danych.

Powyższe dokumenty JSON są bardzo proste, ale ta funkcja mapowania może być również używana z bardziej złożonymi strukturami dokumentów. Rozważmy na przykład inny typ agregacji z naszego przykładowego modelu, który służy do reprezentowania metadanych dotyczących wpisu:

public class PostMetadata
{
    public PostMetadata(int views)
    {
        Views = views;
    }

    public int Views { get; set; }
    public List<SearchTerm> TopSearches { get; } = new();
    public List<Visits> TopGeographies { get; } = new();
    public List<PostUpdate> Updates { get; } = new();
}

public class SearchTerm
{
    public SearchTerm(string term, int count)
    {
        Term = term;
        Count = count;
    }

    public string Term { get; private set; }
    public int Count { get; private set; }
}

public class Visits
{
    public Visits(double latitude, double longitude, int count)
    {
        Latitude = latitude;
        Longitude = longitude;
        Count = count;
    }

    public double Latitude { get; private set; }
    public double Longitude { get; private set; }
    public int Count { get; private set; }
    public List<string>? Browsers { get; set; }
}

public class PostUpdate
{
    public PostUpdate(IPAddress postedFrom, DateTime updatedOn)
    {
        PostedFrom = postedFrom;
        UpdatedOn = updatedOn;
    }

    public IPAddress PostedFrom { get; private set; }
    public string? UpdatedBy { get; init; }
    public DateTime UpdatedOn { get; private set; }
    public List<Commit> Commits { get; } = new();
}

public class Commit
{
    public Commit(DateTime committedOn, string comment)
    {
        CommittedOn = committedOn;
        Comment = comment;
    }

    public DateTime CommittedOn { get; private set; }
    public string Comment { get; set; }
}

Ten typ agregacji zawiera kilka zagnieżdżonych typów i kolekcji. Wywołania i OwnsOneOwnsMany są używane do mapowania tego typu agregacji:

modelBuilder.Entity<Post>().OwnsOne(
    post => post.Metadata, ownedNavigationBuilder =>
    {
        ownedNavigationBuilder.ToJson();
        ownedNavigationBuilder.OwnsMany(metadata => metadata.TopSearches);
        ownedNavigationBuilder.OwnsMany(metadata => metadata.TopGeographies);
        ownedNavigationBuilder.OwnsMany(
            metadata => metadata.Updates,
            ownedOwnedNavigationBuilder => ownedOwnedNavigationBuilder.OwnsMany(update => update.Commits));
    });

Napiwek

ToJson element jest wymagany tylko w zagregowanym katalogu głównym, aby zamapować całą agregację na dokument JSON.

Dzięki temu mapowaniu program EF7 może tworzyć zapytania i wykonywać zapytania w złożonym dokumencie JSON w następujący sposób:

{
  "Views": 5085,
  "TopGeographies": [
    {
      "Browsers": "Firefox, Netscape",
      "Count": 924,
      "Latitude": 110.793,
      "Longitude": 39.2431
    },
    {
      "Browsers": "Firefox, Netscape",
      "Count": 885,
      "Latitude": 133.793,
      "Longitude": 45.2431
    }
  ],
  "TopSearches": [
    {
      "Count": 9359,
      "Term": "Search #1"
    }
  ],
  "Updates": [
    {
      "PostedFrom": "127.0.0.1",
      "UpdatedBy": "Admin",
      "UpdatedOn": "1996-02-17T19:24:29.5429092Z",
      "Commits": []
    },
    {
      "PostedFrom": "127.0.0.1",
      "UpdatedBy": "Admin",
      "UpdatedOn": "2019-11-24T19:24:29.5429093Z",
      "Commits": [
        {
          "Comment": "Commit #1",
          "CommittedOn": "2022-08-21T00:00:00+01:00"
        }
      ]
    },
    {
      "PostedFrom": "127.0.0.1",
      "UpdatedBy": "Admin",
      "UpdatedOn": "1997-05-28T19:24:29.5429097Z",
      "Commits": [
        {
          "Comment": "Commit #1",
          "CommittedOn": "2022-08-21T00:00:00+01:00"
        },
        {
          "Comment": "Commit #2",
          "CommittedOn": "2022-08-21T00:00:00+01:00"
        }
      ]
    }
  ]
}

Uwaga

Mapowanie typów przestrzennych bezpośrednio do formatu JSON nie jest jeszcze obsługiwane. Powyższy dokument używa double wartości jako obejścia. Zagłosuj na typy przestrzenne pomocy technicznej w kolumnach JSON, jeśli jest to coś, co cię interesuje.

Uwaga

Kolekcje mapowania typów pierwotnych na format JSON nie są jeszcze obsługiwane. W powyższym dokumencie użyto konwertera wartości, aby przekształcić kolekcję w ciąg rozdzielony przecinkami. Zagłosuj na Json: dodaj obsługę kolekcji typów pierwotnych, jeśli jest to coś, co cię interesuje.

Uwaga

Mapowanie typów należących do formatu JSON nie jest jeszcze obsługiwane w połączeniu z dziedziczeniem TPT lub TPC. Zagłosuj na właściwości JSON obsługi za pomocą mapowania dziedziczenia TPT/TPC, jeśli jest to coś, co cię interesuje.

Zapytania do kolumn JSON

Zapytania w kolumnach JSON działają tak samo jak wykonywanie zapytań w dowolnym innym typie agregacji w programie EF Core. Oznacza to, że wystarczy użyć LINQ! Oto kilka przykładów.

Zapytanie dla wszystkich autorów, którzy mieszkają w Chigley:

var authorsInChigley = await context.Authors
    .Where(author => author.Contact.Address.City == "Chigley")
    .ToListAsync();

To zapytanie generuje następujący kod SQL podczas korzystania z programu SQL Server:

SELECT [a].[Id], [a].[Name], JSON_QUERY([a].[Contact],'$')
FROM [Authors] AS [a]
WHERE CAST(JSON_VALUE([a].[Contact],'$.Address.City') AS nvarchar(max)) = N'Chigley'

Zwróć uwagę na użycie polecenia JSON_VALUE , aby pobrać element City z Address wewnątrz dokumentu JSON.

Select Umożliwia wyodrębnianie elementów projektu i wyodrębnianie ich z dokumentu JSON:

var postcodesInChigley = await context.Authors
    .Where(author => author.Contact.Address.City == "Chigley")
    .Select(author => author.Contact.Address.Postcode)
    .ToListAsync();

To zapytanie generuje następujący kod SQL:

SELECT CAST(JSON_VALUE([a].[Contact],'$.Address.Postcode') AS nvarchar(max))
FROM [Authors] AS [a]
WHERE CAST(JSON_VALUE([a].[Contact],'$.Address.City') AS nvarchar(max)) = N'Chigley'

Oto przykład, który wykonuje nieco więcej w filtrze i projekcji, a także zamówienia według numeru telefonu w dokumencie JSON:

var orderedAddresses = await context.Authors
    .Where(
        author => (author.Contact.Address.City == "Chigley"
                   && author.Contact.Phone != null)
                  || author.Name.StartsWith("D"))
    .OrderBy(author => author.Contact.Phone)
    .Select(
        author => author.Name + " (" + author.Contact.Address.Street
                  + ", " + author.Contact.Address.City
                  + " " + author.Contact.Address.Postcode + ")")
    .ToListAsync();

To zapytanie generuje następujący kod SQL:

SELECT (((((([a].[Name] + N' (') + CAST(JSON_VALUE([a].[Contact],'$.Address.Street') AS nvarchar(max))) + N', ') + CAST(JSON_VALUE([a].[Contact],'$.Address.City') AS nvarchar(max))) + N' ') + CAST(JSON_VALUE([a].[Contact],'$.Address.Postcode') AS nvarchar(max))) + N')'
FROM [Authors] AS [a]
WHERE (CAST(JSON_VALUE([a].[Contact],'$.Address.City') AS nvarchar(max)) = N'Chigley' AND CAST(JSON_VALUE([a].[Contact],'$.Phone') AS nvarchar(max)) IS NOT NULL) OR ([a].[Name] LIKE N'D%')
ORDER BY CAST(JSON_VALUE([a].[Contact],'$.Phone') AS nvarchar(max))

A gdy dokument JSON zawiera kolekcje, można je rzutować w wynikach:

var postsWithViews = await context.Posts.Where(post => post.Metadata!.Views > 3000)
    .AsNoTracking()
    .Select(
        post => new
        {
            post.Author!.Name, post.Metadata!.Views, Searches = post.Metadata.TopSearches, Commits = post.Metadata.Updates
        })
    .ToListAsync();

To zapytanie generuje następujący kod SQL:

SELECT [a].[Name], CAST(JSON_VALUE([p].[Metadata],'$.Views') AS int), JSON_QUERY([p].[Metadata],'$.TopSearches'), [p].[Id], JSON_QUERY([p].[Metadata],'$.Updates')
FROM [Posts] AS [p]
LEFT JOIN [Authors] AS [a] ON [p].[AuthorId] = [a].[Id]
WHERE CAST(JSON_VALUE([p].[Metadata],'$.Views') AS int) > 3000

Uwaga

Bardziej złożone zapytania obejmujące kolekcje JSON wymagają jsonpath obsługi. Zagłosuj na zapytanie dotyczące ścieżki jsonpath pomocy technicznej, jeśli jest to coś, co cię interesuje.

Napiwek

Rozważ utworzenie indeksów w celu zwiększenia wydajności zapytań w dokumentach JSON. Na przykład zobacz Indeksowanie danych Json podczas korzystania z programu SQL Server.

Aktualizowanie kolumn JSON

SaveChanges i SaveChangesAsync działają w normalny sposób, aby wprowadzać aktualizacje do kolumny JSON. W przypadku rozbudowanych zmian cały dokument zostanie zaktualizowany. Na przykład zastąpienie większości Contact dokumentu dla autora:

var jeremy = await context.Authors.SingleAsync(author => author.Name.StartsWith("Jeremy"));

jeremy.Contact = new() { Address = new("2 Riverside", "Trimbridge", "TB1 5ZS", "UK"), Phone = "01632 88346" };

await context.SaveChangesAsync();

W takim przypadku cały nowy dokument jest przekazywany jako parametr:

info: 8/30/2022 20:21:24.392 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
      Executed DbCommand (2ms) [Parameters=[@p0='{"Phone":"01632 88346","Address":{"City":"Trimbridge","Country":"UK","Postcode":"TB1 5ZS","Street":"2 Riverside"}}' (Nullable = false) (Size = 114), @p1='2'], CommandType='Text', CommandTimeout='30']

Który jest następnie używany w języku UPDATE SQL:

UPDATE [Authors] SET [Contact] = @p0
OUTPUT 1
WHERE [Id] = @p1;

Jeśli jednak tylko dokument podrzędny zostanie zmieniony, program EF Core użyje JSON_MODIFY polecenia w celu zaktualizowania tylko dokumentu podrzędnego. Na przykład zmiana Address wewnątrz Contact dokumentu:

var brice = await context.Authors.SingleAsync(author => author.Name.StartsWith("Brice"));

brice.Contact.Address = new("4 Riverside", "Trimbridge", "TB1 5ZS", "UK");

await context.SaveChangesAsync();

Generuje następujące parametry:

info: 10/2/2022 15:51:15.895 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
      Executed DbCommand (2ms) [Parameters=[@p0='{"City":"Trimbridge","Country":"UK","Postcode":"TB1 5ZS","Street":"4 Riverside"}' (Nullable = false) (Size = 80), @p1='5'], CommandType='Text', CommandTimeout='30']

Który jest używany w UPDATE wywołaniu JSON_MODIFY :

UPDATE [Authors] SET [Contact] = JSON_MODIFY([Contact], 'strict $.Address', JSON_QUERY(@p0))
OUTPUT 1
WHERE [Id] = @p1;

Na koniec, jeśli tylko jedna właściwość zostanie zmieniona, program EF Core ponownie użyje polecenia "JSON_MODIFY", tym razem, aby zastosować poprawkę tylko zmienionej wartości właściwości. Przykład:

var arthur = await context.Authors.SingleAsync(author => author.Name.StartsWith("Arthur"));

arthur.Contact.Address.Country = "United Kingdom";

await context.SaveChangesAsync();

Generuje następujące parametry:

info: 10/2/2022 15:54:05.112 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
      Executed DbCommand (2ms) [Parameters=[@p0='["United Kingdom"]' (Nullable = false) (Size = 18), @p1='4'], CommandType='Text', CommandTimeout='30']

Które są ponownie używane z elementem JSON_MODIFY:

UPDATE [Authors] SET [Contact] = JSON_MODIFY([Contact], 'strict $.Address.Country', JSON_VALUE(@p0, '$[0]'))
OUTPUT 1
WHERE [Id] = @p1;

ExecuteUpdate i ExecuteDelete (aktualizacje zbiorcze)

Domyślnie program EF Core śledzi zmiany w jednostkach, a następnie wysyła aktualizacje do bazy danych po wywołaniu jednej z SaveChanges metod. Zmiany są wysyłane tylko dla właściwości i relacji, które rzeczywiście uległy zmianie. Ponadto śledzone jednostki pozostają zsynchronizowane ze zmianami wysłanymi do bazy danych. Ten mechanizm jest wydajnym i wygodnym sposobem wysyłania wstawień, aktualizacji i usuwania ogólnego przeznaczenia do bazy danych. Te zmiany są również dzielone na partie w celu zmniejszenia liczby rund bazy danych.

Jednak czasami przydatne jest wykonywanie poleceń aktualizacji lub usuwania w bazie danych bez angażowania monitora zmian. Program EF7 umożliwia korzystanie z nowych ExecuteUpdate metod i ExecuteDelete . Te metody są stosowane do zapytania LINQ i będą aktualizować lub usuwać jednostki w bazie danych na podstawie wyników tego zapytania. Wiele jednostek można zaktualizować za pomocą jednego polecenia, a jednostki nie są ładowane do pamięci, co oznacza, że może to spowodować bardziej wydajne aktualizacje i usunięcie.

Należy jednak pamiętać, że:

  • Określone zmiany, które należy wprowadzić, muszą być określone jawnie; nie są one automatycznie wykrywane przez program EF Core.
  • Wszystkie śledzone jednostki nie będą synchronizowane.
  • Może być konieczne wysłanie dodatkowych poleceń w odpowiedniej kolejności, aby nie naruszać ograniczeń bazy danych. Na przykład usunięcie zależności przed usunięciem podmiotu zabezpieczeń.

Wszystko to oznacza, że ExecuteUpdate metody i ExecuteDelete uzupełniają, a nie zastępują istniejącego SaveChanges mechanizmu.

Przykłady podstawowe ExecuteDelete

Napiwek

Pokazany tutaj kod pochodzi z pliku ExecuteDeleteSample.cs.

Wywołanie ExecuteDelete metody lub ExecuteDeleteAsync na DbSet serwerze natychmiast usuwa wszystkie jednostki z DbSet bazy danych. Aby na przykład usunąć wszystkie Tag jednostki:

await context.Tags.ExecuteDeleteAsync();

Spowoduje to wykonanie następującego kodu SQL podczas korzystania z programu SQL Server:

DELETE FROM [t]
FROM [Tags] AS [t]

Co ciekawe, zapytanie może zawierać filtr. Przykład:

await context.Tags.Where(t => t.Text.Contains(".NET")).ExecuteDeleteAsync();

Spowoduje to wykonanie następującego kodu SQL:

DELETE FROM [t]
FROM [Tags] AS [t]
WHERE [t].[Text] LIKE N'%.NET%'

Zapytanie może również używać bardziej złożonych filtrów, w tym nawigacji do innych typów. Aby na przykład usunąć tagi tylko ze starych wpisów w blogu:

await context.Tags.Where(t => t.Posts.All(e => e.PublishedOn.Year < 2022)).ExecuteDeleteAsync();

Które wykonuje:

DELETE FROM [t]
FROM [Tags] AS [t]
WHERE NOT EXISTS (
    SELECT 1
    FROM [PostTag] AS [p]
    INNER JOIN [Posts] AS [p0] ON [p].[PostsId] = [p0].[Id]
    WHERE [t].[Id] = [p].[TagsId] AND NOT (DATEPART(year, [p0].[PublishedOn]) < 2022))

Przykłady podstawowe ExecuteUpdate

Napiwek

Pokazany tutaj kod pochodzi z pliku ExecuteUpdateSample.cs.

ExecuteUpdate i ExecuteUpdateAsync zachowują się w bardzo podobny sposób do ExecuteDelete metod. Główną różnicą jest to, że aktualizacja wymaga znajomościwłaściwości do zaktualizowania i sposobu ich aktualizowania. Jest to osiągane przy użyciu co najmniej jednego wywołania metody SetProperty. Na przykład, aby zaktualizować każdy Name blog:

await context.Blogs.ExecuteUpdateAsync(
    s => s.SetProperty(b => b.Name, b => b.Name + " *Featured!*"));

Pierwszy parametr określa SetProperty , która właściwość ma być aktualizowana; w tym przypadku Blog.Name. Drugi parametr określa sposób obliczania nowej wartości; w tym przypadku przez pobranie istniejącej wartości i dołączenie "*Featured!*"elementu . Wynikowy kod SQL to:

UPDATE [b]
SET [b].[Name] = [b].[Name] + N' *Featured!*'
FROM [Blogs] AS [b]

Podobnie jak w przypadku ExecuteDeleteelementu , zapytanie może służyć do filtrowania, które jednostki są aktualizowane. Ponadto wiele wywołań programu SetProperty może służyć do aktualizowania więcej niż jednej właściwości w jednostce docelowej. Na przykład, aby zaktualizować Title i Content wszystkich wpisów opublikowanych przed 2022 r.:

await context.Posts
    .Where(p => p.PublishedOn.Year < 2022)
    .ExecuteUpdateAsync(s => s
        .SetProperty(b => b.Title, b => b.Title + " (" + b.PublishedOn.Year + ")")
        .SetProperty(b => b.Content, b => b.Content + " ( This content was published in " + b.PublishedOn.Year + ")"));

W takim przypadku wygenerowany język SQL jest nieco bardziej skomplikowany:

UPDATE [p]
SET [p].[Content] = (([p].[Content] + N' ( This content was published in ') + COALESCE(CAST(DATEPART(year, [p].[PublishedOn]) AS nvarchar(max)), N'')) + N')',
    [p].[Title] = (([p].[Title] + N' (') + COALESCE(CAST(DATEPART(year, [p].[PublishedOn]) AS nvarchar(max)), N'')) + N')'
FROM [Posts] AS [p]
WHERE DATEPART(year, [p].[PublishedOn]) < 2022

Na koniec, podobnie jak w przypadku ExecuteDeletemetody , filtr może odwoływać się do innych tabel. Aby na przykład zaktualizować wszystkie tagi ze starych wpisów:

await context.Tags
    .Where(t => t.Posts.All(e => e.PublishedOn.Year < 2022))
    .ExecuteUpdateAsync(s => s.SetProperty(t => t.Text, t => t.Text + " (old)"));

Które generuje:

UPDATE [t]
SET [t].[Text] = [t].[Text] + N' (old)'
FROM [Tags] AS [t]
WHERE NOT EXISTS (
    SELECT 1
    FROM [PostTag] AS [p]
    INNER JOIN [Posts] AS [p0] ON [p].[PostsId] = [p0].[Id]
    WHERE [t].[Id] = [p].[TagsId] AND NOT (DATEPART(year, [p0].[PublishedOn]) < 2022))

Aby uzyskać więcej informacji i przykładów kodu w systemach ExecuteUpdate i ExecuteDelete, zobacz ExecuteUpdate i ExecuteDelete.

Dziedziczenie i wiele tabel

ExecuteUpdate i ExecuteDelete może działać tylko na jednej tabeli. Ma to wpływ na pracę z różnymi strategiami mapowania dziedziczenia. Ogólnie rzecz biorąc, podczas korzystania ze strategii mapowania TPH nie ma problemów, ponieważ istnieje tylko jedna tabela do zmodyfikowania. Na przykład usunięcie wszystkich FeaturedPost jednostek:

await context.Set<FeaturedPost>().ExecuteDeleteAsync();

Generuje następujący kod SQL podczas korzystania z mapowania TPH:

DELETE FROM [p]
FROM [Posts] AS [p]
WHERE [p].[Discriminator] = N'FeaturedPost'

W przypadku korzystania ze strategii mapowania TPC nie ma również żadnych problemów, ponieważ potrzebne są tylko zmiany w pojedynczej tabeli:

DELETE FROM [f]
FROM [FeaturedPosts] AS [f]

Jednak próba użycia strategii mapowania TPT zakończy się niepowodzeniem, ponieważ wymaga usunięcia wierszy z dwóch różnych tabel.

Dodanie filtru do zapytania często oznacza, że operacja zakończy się niepowodzeniem zarówno ze strategiami TPC, jak i TPT. Jest to ponownie spowodowane koniecznością usunięcia wierszy z wielu tabel. Na przykład to zapytanie:

await context.Posts.Where(p => p.Author!.Name.StartsWith("Arthur")).ExecuteDeleteAsync();

Generuje następujący kod SQL podczas korzystania z funkcji TPH:

DELETE FROM [p]
FROM [Posts] AS [p]
LEFT JOIN [Authors] AS [a] ON [p].[AuthorId] = [a].[Id]
WHERE [a].[Name] IS NOT NULL AND ([a].[Name] LIKE N'Arthur%')

Ale kończy się niepowodzeniem w przypadku korzystania z TPC lub TPT.

Napiwek

Problem nr 10879 śledzi dodawanie obsługi automatycznego wysyłania wielu poleceń w tych scenariuszach. Zagłosuj na ten problem, jeśli jest to coś, co chcesz zobaczyć zaimplementowane.

ExecuteDelete i relacje

Jak wspomniano powyżej, może być konieczne usunięcie lub zaktualizowanie jednostek zależnych przed usunięciem podmiotu zabezpieczeń relacji. Na przykład każda z nich Post jest zależna od skojarzonej z nią wartości Author. Oznacza to, że nie można usunąć autora, jeśli wpis nadal odwołuje się do niego; Spowoduje to naruszenie ograniczenia klucza obcego w bazie danych. Na przykład próba:

await context.Authors.ExecuteDeleteAsync();

Spowoduje to następujący wyjątek w programie SQL Server:

Microsoft.Data.SqlClient.SqlException (0x80131904): instrukcja DELETE powoduje konflikt z ograniczeniem REFERENCE "FK_Posts_Authors_AuthorId". Konflikt wystąpił w bazie danych "TphBlogsContext", tabeli "dbo". Posty", kolumna "AuthorId". Instrukcja została zakończona.

Aby rozwiązać ten problem, musimy najpierw usunąć wpisy lub zerwać relację między każdym wpisem a jego autorem, ustawiając AuthorId właściwość klucza obcego na null. Na przykład przy użyciu opcji usuwania:

await context.Posts.TagWith("Deleting posts...").ExecuteDeleteAsync();
await context.Authors.TagWith("Deleting authors...").ExecuteDeleteAsync();

Napiwek

TagWith Może służyć do tagowania ExecuteDelete lub ExecuteUpdate w taki sam sposób, jak taguje normalne zapytania.

Spowoduje to wykonanie dwóch oddzielnych poleceń; pierwszy, aby usunąć zależności:

-- Deleting posts...

DELETE FROM [p]
FROM [Posts] AS [p]

A drugi, aby usunąć podmioty zabezpieczeń:

-- Deleting authors...

DELETE FROM [a]
FROM [Authors] AS [a]

Ważne

Wiele ExecuteDelete poleceń i ExecuteUpdate nie będzie domyślnie zawartych w jednej transakcji. Interfejsy API transakcji DbContext mogą być jednak używane w normalny sposób, aby opakowować te polecenia w transakcji.

Napiwek

Wysyłanie tych poleceń w jednej rundzie zależy od problemu #10879. Zagłosuj na ten problem, jeśli jest to coś, co chcesz zobaczyć zaimplementowane.

Konfigurowanie usuwania kaskadowego w bazie danych może być bardzo przydatne tutaj. W naszym modelu wymagana jest relacja między elementami Blog i Post , co powoduje skonfigurowanie kaskadowego usuwania przez platformę EF Core zgodnie z konwencją. Oznacza to, że gdy blog zostanie usunięty z bazy danych, wszystkie jego zależne wpisy również zostaną usunięte. Następnie następuje usunięcie wszystkich blogów i wpisów, których potrzebujemy tylko do usunięcia blogów:

await context.Blogs.ExecuteDeleteAsync();

Spowoduje to wykonanie następujących czynności w języku SQL:

DELETE FROM [b]
FROM [Blogs] AS [b]

Co, ponieważ usuwa blog, spowoduje również usunięcie wszystkich powiązanych wpisów przez skonfigurowane kaskadowe usunięcie.

Szybsze zapisywanie zmian

W ef7 wydajność SaveChanges i SaveChangesAsync została znacznie zwiększona. W niektórych scenariuszach zapisywanie zmian jest teraz maksymalnie cztery razy szybsze niż w przypadku programu EF Core 6.0!

Większość tych ulepszeń pochodzi z następujących elementów:

  • Wykonywanie mniejszej liczby przejazdów do bazy danych
  • Szybsze generowanie kodu SQL

Poniżej przedstawiono kilka przykładów tych ulepszeń.

Uwaga

Zobacz Ogłoszenie programu Entity Framework Core 7 Preview 6: Performance Edition na blogu platformy .NET, aby zapoznać się ze szczegółowym omówieniem tych zmian.

Napiwek

Pokazany tutaj kod pochodzi z pliku SaveChangesPerformanceSample.cs.

Niepotrzebne transakcje są wyeliminowane

Wszystkie nowoczesne relacyjne bazy danych gwarantują transakcyjność (większość) pojedynczych instrukcji SQL. Oznacza to, że instrukcja nigdy nie zostanie ukończona tylko częściowo, nawet jeśli wystąpi błąd. Program EF7 unika uruchamiania jawnej transakcji w tych przypadkach.

Na przykład zapoznaj się z rejestrowaniem dla następującego wywołania metody SaveChanges:

await context.AddAsync(new Blog { Name = "MyBlog" });
await context.SaveChangesAsync();

Pokazuje, że w programie EF Core 6.0 polecenie jest opakowane przez polecenia, INSERT aby rozpocząć, a następnie zatwierdzić transakcję:

dbug: 9/29/2022 11:43:09.196 RelationalEventId.TransactionStarted[20200] (Microsoft.EntityFrameworkCore.Database.Transaction)
      Began transaction with isolation level 'ReadCommitted'.
info: 9/29/2022 11:43:09.265 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
      Executed DbCommand (27ms) [Parameters=[@p0='MyBlog' (Nullable = false) (Size = 4000)], CommandType='Text', CommandTimeout='30']
      SET NOCOUNT ON;
      INSERT INTO [Blogs] ([Name])
      VALUES (@p0);
      SELECT [Id]
      FROM [Blogs]
      WHERE @@ROWCOUNT = 1 AND [Id] = scope_identity();
dbug: 9/29/2022 11:43:09.297 RelationalEventId.TransactionCommitted[20202] (Microsoft.EntityFrameworkCore.Database.Transaction)
      Committed transaction.

Program EF7 wykrywa, że transakcja nie jest tutaj potrzebna, a więc usuwa następujące wywołania:

info: 9/29/2022 11:42:34.776 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
      Executed DbCommand (25ms) [Parameters=[@p0='MyBlog' (Nullable = false) (Size = 4000)], CommandType='Text', CommandTimeout='30']
      SET IMPLICIT_TRANSACTIONS OFF;
      SET NOCOUNT ON;
      INSERT INTO [Blogs] ([Name])
      OUTPUT INSERTED.[Id]
      VALUES (@p0);

Spowoduje to usunięcie dwóch rund baz danych, co może mieć ogromną różnicę w ogólnej wydajności, zwłaszcza gdy opóźnienie wywołań bazy danych jest wysokie. W typowych systemach produkcyjnych baza danych nie znajduje się na tej samej maszynie co aplikacja. Oznacza to, że opóźnienie jest często stosunkowo wysokie, co czyni tę optymalizację szczególnie skuteczną w rzeczywistych systemach produkcyjnych.

Ulepszono język SQL na potrzeby prostego wstawiania tożsamości

Powyższy przypadek wstawia pojedynczy wiersz z kolumną IDENTITY klucza i nie ma innych wartości wygenerowanych przez bazę danych. Program EF7 upraszcza język SQL w tym przypadku przy użyciu polecenia OUTPUT INSERTED. Chociaż to uproszczenie nie jest prawidłowe w wielu innych przypadkach, nadal ważne jest, aby poprawić, ponieważ tego rodzaju wstawianie jednorzędowe jest bardzo powszechne w wielu aplikacjach.

Wstawianie wielu wierszy

W programie EF Core 6.0 domyślne podejście do wstawiania wielu wierszy było spowodowane ograniczeniami obsługi programu SQL Server dla tabel z wyzwalaczami. Chcieliśmy upewnić się, że środowisko domyślne działało nawet dla mniejszości użytkowników z wyzwalaczami w tabelach. Oznaczało to, że nie można użyć prostej OUTPUT klauzuli, ponieważ w programie SQL Server nie działa to z wyzwalaczami. Zamiast tego podczas wstawiania wielu jednostek program EF Core 6.0 wygenerował nieco dość splotowy kod SQL. Na przykład to wywołanie metody :SaveChanges

for (var i = 0; i < 4; i++)
{
    await context.AddAsync(new Blog { Name = "Foo" + i });
}

await context.SaveChangesAsync();

Wyniki następujących akcji podczas uruchamiania względem programu SQL Server z programem EF Core 6.0:

dbug: 9/30/2022 17:19:51.919 RelationalEventId.TransactionStarted[20200] (Microsoft.EntityFrameworkCore.Database.Transaction)
      Began transaction with isolation level 'ReadCommitted'.
info: 9/30/2022 17:19:51.993 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
      Executed DbCommand (27ms) [Parameters=[@p0='Foo0' (Nullable = false) (Size = 4000), @p1='Foo1' (Nullable = false) (Size = 4000), @p2='Foo2' (Nullable = false) (Size = 4000), @p3='Foo3' (Nullable = false) (Size = 4000)], CommandType='Text', CommandTimeout='30']
      SET NOCOUNT ON;
      DECLARE @inserted0 TABLE ([Id] int, [_Position] [int]);
      MERGE [Blogs] USING (
      VALUES (@p0, 0),
      (@p1, 1),
      (@p2, 2),
      (@p3, 3)) AS i ([Name], _Position) ON 1=0
      WHEN NOT MATCHED THEN
      INSERT ([Name])
      VALUES (i.[Name])
      OUTPUT INSERTED.[Id], i._Position
      INTO @inserted0;

      SELECT [i].[Id] FROM @inserted0 i
      ORDER BY [i].[_Position];
dbug: 9/30/2022 17:19:52.023 RelationalEventId.TransactionCommitted[20202] (Microsoft.EntityFrameworkCore.Database.Transaction)
      Committed transaction.

Ważne

Mimo że jest to skomplikowane, przetwarzanie wsadowe wielu wstawek w ten sposób jest nadal znacznie szybsze niż wysyłanie pojedynczego polecenia dla każdego wstawiania.

W programie EF7 nadal można uzyskać ten język SQL, jeśli tabele zawierają wyzwalacze, ale w przypadku typowego przypadku generujemy znacznie wydajniejsze polecenia, jeśli nadal są one nieco złożone:

info: 9/30/2022 17:40:37.612 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
      Executed DbCommand (4ms) [Parameters=[@p0='Foo0' (Nullable = false) (Size = 4000), @p1='Foo1' (Nullable = false) (Size = 4000), @p2='Foo2' (Nullable = false) (Size = 4000), @p3='Foo3' (Nullable = false) (Size = 4000)], CommandType='Text', CommandTimeout='30']
      SET IMPLICIT_TRANSACTIONS OFF;
      SET NOCOUNT ON;
      MERGE [Blogs] USING (
      VALUES (@p0, 0),
      (@p1, 1),
      (@p2, 2),
      (@p3, 3)) AS i ([Name], _Position) ON 1=0
      WHEN NOT MATCHED THEN
      INSERT ([Name])
      VALUES (i.[Name])
      OUTPUT INSERTED.[Id], i._Position;

Transakcja nie istnieje, podobnie jak w przypadku pojedynczego wstawiania, ponieważ MERGE jest pojedynczą instrukcją chronioną przez niejawną transakcję. Ponadto tabela tymczasowa nie istnieje, a klauzula OUTPUT wysyła teraz wygenerowane identyfikatory bezpośrednio do klienta. Może to być cztery razy szybsze niż w programie EF Core 6.0, w zależności od czynników środowiskowych, takich jak opóźnienie między aplikacją a bazą danych.

Wyzwalacze

Jeśli tabela zawiera wyzwalacze, wywołanie metody SaveChanges w powyższym kodzie zgłosi wyjątek:

Nieobsługiwany wyjątek. Microsoft.EntityFrameworkCore.DbUpdateException:
Nie można zapisać zmian, ponieważ tabela docelowa zawiera wyzwalacze bazy danych. Aby uzyskać więcej informacji, skonfiguruj https://aka.ms/efcore-docs-sqlserver-save-changes-and-triggers odpowiedni typ jednostki.
>--- Microsoft.Data.SqlClient.SqlException (0x80131904):
tabela docelowa "BlogsWithTriggers" instrukcji DML nie może mieć żadnych włączonych wyzwalaczy, jeśli instrukcja zawiera klauzulę OUTPUT bez klauzuli INTO.

Poniższy kod może służyć do informowania platformy EF Core o tym, że tabela ma wyzwalacz:

modelBuilder
    .Entity<BlogWithTrigger>()
    .ToTable(tb => tb.HasTrigger("TRG_InsertUpdateBlog"));

Program EF7 powróci do programu EF Core 6.0 SQL podczas wysyłania poleceń wstawiania i aktualizowania tej tabeli.

Aby uzyskać więcej informacji, w tym konwencję automatycznego konfigurowania wszystkich mapowanych tabel z wyzwalaczami, zobacz Tabele programu SQL Server z wyzwalaczami wymagają teraz specjalnej konfiguracji programu EF Core w dokumentacji zmian powodujących niezgodność w programie EF7.

Mniej rund na potrzeby wstawiania grafów

Rozważ wstawienie grafu jednostek zawierających nową jednostkę główną, a także nowe jednostki zależne z kluczami obcymi odwołujące się do nowego podmiotu zabezpieczeń. Przykład:

await context.AddAsync(
    new Blog { Name = "MyBlog", Posts = { new() { Title = "My first post" }, new() { Title = "My second post" } } });
await context.SaveChangesAsync();

Jeśli klucz podstawowy podmiotu zabezpieczeń jest generowany przez bazę danych, wartość ustawiona dla klucza obcego w zależnych nie jest znana, dopóki podmiot zabezpieczeń nie zostanie wstawiony. Program EF Core generuje dwa rundy dla tego - jeden w celu wstawienia podmiotu zabezpieczeń i odzyskania nowego klucza podstawowego, a drugi w celu wstawienia zależności z zestawem wartości klucza obcego. A ponieważ istnieją dwie instrukcje dotyczące tego, wymagana jest transakcja, co oznacza, że istnieją w sumie cztery rundy:

dbug: 10/1/2022 13:12:02.517 RelationalEventId.TransactionStarted[20200] (Microsoft.EntityFrameworkCore.Database.Transaction)
      Began transaction with isolation level 'ReadCommitted'.
info: 10/1/2022 13:12:02.517 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
      Executed DbCommand (0ms) [Parameters=[@p0='MyBlog' (Nullable = false) (Size = 4000)], CommandType='Text', CommandTimeout='30']
      SET IMPLICIT_TRANSACTIONS OFF;
      SET NOCOUNT ON;
      INSERT INTO [Blogs] ([Name])
      OUTPUT INSERTED.[Id]
      VALUES (@p0);
info: 10/1/2022 13:12:02.529 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
      Executed DbCommand (5ms) [Parameters=[@p1='6', @p2='My first post' (Nullable = false) (Size = 4000), @p3='6', @p4='My second post' (Nullable = false) (Size = 4000)], CommandType='Text', CommandTimeout='30']
      SET IMPLICIT_TRANSACTIONS OFF;
      SET NOCOUNT ON;
      MERGE [Post] USING (
      VALUES (@p1, @p2, 0),
      (@p3, @p4, 1)) AS i ([BlogId], [Title], _Position) ON 1=0
      WHEN NOT MATCHED THEN
      INSERT ([BlogId], [Title])
      VALUES (i.[BlogId], i.[Title])
      OUTPUT INSERTED.[Id], i._Position;
dbug: 10/1/2022 13:12:02.531 RelationalEventId.TransactionCommitted[20202] (Microsoft.EntityFrameworkCore.Database.Transaction)
      Committed transaction.

Jednak w niektórych przypadkach wartość klucza podstawowego jest znana przed wstawieniem podmiotu zabezpieczeń. Obejmuje on:

  • Wartości klucza, które nie są generowane automatycznie
  • Wartości klucza generowane na kliencie, takie jak Guid klucze
  • Wartości klucza generowane na serwerze w partiach, takie jak w przypadku korzystania z generatora wartości hi-lo

W programie EF7 te przypadki są teraz zoptymalizowane pod kątem jednej rundy. Na przykład w powyższym przypadku w programie SQL Server Blog.Id klucz podstawowy można skonfigurować do używania strategii generowania hi-lo:

modelBuilder.Entity<Blog>().Property(e => e.Id).UseHiLo();
modelBuilder.Entity<Post>().Property(e => e.Id).UseHiLo();

Wywołanie SaveChanges z góry jest teraz zoptymalizowane pod kątem pojedynczej rundy dla wstawiania.

dbug: 10/1/2022 21:51:55.805 RelationalEventId.TransactionStarted[20200] (Microsoft.EntityFrameworkCore.Database.Transaction)
      Began transaction with isolation level 'ReadCommitted'.
info: 10/1/2022 21:51:55.806 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
      Executed DbCommand (0ms) [Parameters=[@p0='9', @p1='MyBlog' (Nullable = false) (Size = 4000), @p2='10', @p3='9', @p4='My first post' (Nullable = false) (Size = 4000), @p5='11', @p6='9', @p7='My second post' (Nullable = false) (Size = 4000)], CommandType='Text', CommandTimeout='30']
      SET NOCOUNT ON;
      INSERT INTO [Blogs] ([Id], [Name])
      VALUES (@p0, @p1);
      INSERT INTO [Posts] ([Id], [BlogId], [Title])
      VALUES (@p2, @p3, @p4),
      (@p5, @p6, @p7);
dbug: 10/1/2022 21:51:55.807 RelationalEventId.TransactionCommitted[20202] (Microsoft.EntityFrameworkCore.Database.Transaction)
      Committed transaction.

Zwróć uwagę, że transakcja jest nadal potrzebna tutaj. Dzieje się tak, ponieważ wstawki są tworzone w dwóch oddzielnych tabelach.

Program EF7 używa również jednej partii w innych przypadkach, w których program EF Core 6.0 tworzy więcej niż jedną partię. Na przykład podczas usuwania i wstawiania wierszy do tej samej tabeli.

Wartość saveChanges

Jak pokazano w niektórych przykładach, zapisywanie wyników w bazie danych może być złożoną działalnością biznesową. W tym miejscu użycie czegoś takiego jak EF Core naprawdę pokazuje jego wartość. EF Core:

  • Wsaduje wiele poleceń wstawiania, aktualizowania i usuwania w celu zmniejszenia liczby rund
  • Określa, czy wymagana jest jawna transakcja, czy nie
  • Określa kolejność wstawiania, aktualizowania i usuwania jednostek, aby ograniczenia bazy danych nie zostały naruszone
  • Gwarantuje, że wygenerowane wartości bazy danych są zwracane wydajnie i propagowane z powrotem do jednostek
  • Automatycznie ustawia wartości kluczy obcych przy użyciu wartości wygenerowanych dla kluczy podstawowych
  • Wykrywanie konfliktów współbieżności

Ponadto różne systemy baz danych wymagają różnych baz danych SQL dla wielu z tych przypadków. Dostawca bazy danych EF Core współpracuje z platformą EF Core, aby zapewnić wysyłanie poprawnych i wydajnych poleceń dla każdego przypadku.

Mapowanie dziedziczenia typu tabeli na beton (TPC)

Domyślnie program EF Core mapuje hierarchię dziedziczenia typów platformy .NET na jedną tabelę bazy danych. Jest to nazywane strategią mapowania tabeli na hierarchię (TPH ). Program EF Core 5.0 wprowadził strategię typu tabeli (TPT ), która obsługuje mapowanie każdego typu platformy .NET na inną tabelę bazy danych. Program EF7 wprowadza strategię TPC (table-per-concrete-type). TPC mapuje również typy platformy .NET na różne tabele, ale w sposób, który rozwiązuje niektóre typowe problemy z wydajnością strategii TPT.

Napiwek

Pokazany tutaj kod pochodzi z pliku TpcInheritanceSample.cs.

Napiwek

Zespół EF pokazał i szczegółowo omówił mapowanie TPC w odcinku standupu społeczności danych platformy .NET. Podobnie jak w przypadku wszystkich odcinków Community Standup, możesz obejrzeć odcinek TPC teraz na YouTube.

Schemat bazy danych TPC

Strategia TPC jest podobna do strategii TPT, z tą różnicą, że dla każdego konkretnego typu w hierarchii tworzona jest inna tabela, ale tabele nietworzone dla typów abstrakcyjnych — stąd nazwa "tabela-typ-beton". Podobnie jak w przypadku TPT, sama tabela wskazuje typ zapisanego obiektu. Jednak w przeciwieństwie do mapowania TPT każda tabela zawiera kolumny dla każdej właściwości w typie betonowym i jego typach podstawowych. Schematy bazy danych TPC są zdenormalizowane.

Rozważ na przykład mapowanie tej hierarchii:

public abstract class Animal
{
    protected Animal(string name)
    {
        Name = name;
    }

    public int Id { get; set; }
    public string Name { get; set; }
    public abstract string Species { get; }

    public Food? Food { get; set; }
}

public abstract class Pet : Animal
{
    protected Pet(string name)
        : base(name)
    {
    }

    public string? Vet { get; set; }

    public ICollection<Human> Humans { get; } = new List<Human>();
}

public class FarmAnimal : Animal
{
    public FarmAnimal(string name, string species)
        : base(name)
    {
        Species = species;
    }

    public override string Species { get; }

    [Precision(18, 2)]
    public decimal Value { get; set; }

    public override string ToString()
        => $"Farm animal '{Name}' ({Species}/{Id}) worth {Value:C} eats {Food?.ToString() ?? "<Unknown>"}";
}

public class Cat : Pet
{
    public Cat(string name, string educationLevel)
        : base(name)
    {
        EducationLevel = educationLevel;
    }

    public string EducationLevel { get; set; }
    public override string Species => "Felis catus";

    public override string ToString()
        => $"Cat '{Name}' ({Species}/{Id}) with education '{EducationLevel}' eats {Food?.ToString() ?? "<Unknown>"}";
}

public class Dog : Pet
{
    public Dog(string name, string favoriteToy)
        : base(name)
    {
        FavoriteToy = favoriteToy;
    }

    public string FavoriteToy { get; set; }
    public override string Species => "Canis familiaris";

    public override string ToString()
        => $"Dog '{Name}' ({Species}/{Id}) with favorite toy '{FavoriteToy}' eats {Food?.ToString() ?? "<Unknown>"}";
}

public class Human : Animal
{
    public Human(string name)
        : base(name)
    {
    }

    public override string Species => "Homo sapiens";

    public Animal? FavoriteAnimal { get; set; }
    public ICollection<Pet> Pets { get; } = new List<Pet>();

    public override string ToString()
        => $"Human '{Name}' ({Species}/{Id}) with favorite animal '{FavoriteAnimal?.Name ?? "<Unknown>"}'" +
           $" eats {Food?.ToString() ?? "<Unknown>"}";
}

W przypadku korzystania z programu SQL Server tabele utworzone dla tej hierarchii to:

CREATE TABLE [Cats] (
    [Id] int NOT NULL DEFAULT (NEXT VALUE FOR [AnimalSequence]),
    [Name] nvarchar(max) NOT NULL,
    [FoodId] uniqueidentifier NULL,
    [Vet] nvarchar(max) NULL,
    [EducationLevel] nvarchar(max) NOT NULL,
    CONSTRAINT [PK_Cats] PRIMARY KEY ([Id]));

CREATE TABLE [Dogs] (
    [Id] int NOT NULL DEFAULT (NEXT VALUE FOR [AnimalSequence]),
    [Name] nvarchar(max) NOT NULL,
    [FoodId] uniqueidentifier NULL,
    [Vet] nvarchar(max) NULL,
    [FavoriteToy] nvarchar(max) NOT NULL,
    CONSTRAINT [PK_Dogs] PRIMARY KEY ([Id]));

CREATE TABLE [FarmAnimals] (
    [Id] int NOT NULL DEFAULT (NEXT VALUE FOR [AnimalSequence]),
    [Name] nvarchar(max) NOT NULL,
    [FoodId] uniqueidentifier NULL,
    [Value] decimal(18,2) NOT NULL,
    [Species] nvarchar(max) NOT NULL,
    CONSTRAINT [PK_FarmAnimals] PRIMARY KEY ([Id]));

CREATE TABLE [Humans] (
    [Id] int NOT NULL DEFAULT (NEXT VALUE FOR [AnimalSequence]),
    [Name] nvarchar(max) NOT NULL,
    [FoodId] uniqueidentifier NULL,
    [FavoriteAnimalId] int NULL,
    CONSTRAINT [PK_Humans] PRIMARY KEY ([Id]));

Zwróć uwagę, że:

  • Nie ma tabel dla Animal typów lub Pet , ponieważ znajdują się abstract one w modelu obiektów. Należy pamiętać, że język C# nie zezwala na wystąpienia typów abstrakcyjnych i dlatego nie ma sytuacji, w której wystąpienie typu abstrakcyjnego zostanie zapisane w bazie danych.

  • Mapowanie właściwości w typach podstawowych jest powtarzane dla każdego konkretnego typu. Na przykład każda tabela ma kolumnę Name , a zarówno Koty, jak i Psy mają kolumnę Vet .

  • Zapisanie niektórych danych w tej bazie danych powoduje wykonanie następujących czynności:

Tabela Cats

Id Nazwisko Identyfikator żywności Vet EducationLevel
1 Alicja 99ca3e98-b26d-4a0c-d4ae-08da7aca624f Pengelly MBA
2 Mac 99ca3e98-b26d-4a0c-d4ae-08da7aca624f Pengelly Wieku przedszkolnym
8 Baxter 5dc5019e-6f72-454b-d4b0-08da7aca624f Bothell Pet Hospital Bsc

Stół dla psów

Id Nazwisko Identyfikator żywności Vet UlubioneToy
3 Toast 011aaf6f-d588-4fad-d4ac-08da7aca624f Pengelly Pan Wiewiórka

Tabela FarmAnimals

Id Nazwisko Identyfikator żywności Wartość Gatunki
4 Clyde 1d495075-f527-4498-d4af-08da7aca624f 100.00 Equus africanus asinus

Stół dla ludzi

Id Nazwisko Identyfikator żywności FavoriteAnimalId
5 Wendy 5418fd81-7660-432f-d4b1-08da7aca624f 2
6 Arthur 59b495d4-0414-46bf-d4ad-08da7aca624f 1
9 Katie null 8

Zwróć uwagę, że w przeciwieństwie do mapowania TPT wszystkie informacje dotyczące pojedynczego obiektu znajdują się w jednej tabeli. I, w przeciwieństwie do mapowania TPH, nie ma kombinacji kolumn i wierszy w żadnej tabeli, w której nigdy nie jest używany przez model. Poniżej zobaczymy, jak te cechy mogą być ważne dla zapytań i magazynu.

Konfigurowanie dziedziczenia TPC

Wszystkie typy w hierarchii dziedziczenia muszą być jawnie uwzględnione w modelu podczas mapowania hierarchii za pomocą programu EF Core. Można to zrobić, tworząc DbSet właściwości dla DbContext każdego typu:

public DbSet<Animal> Animals => Set<Animal>();
public DbSet<Pet> Pets => Set<Pet>();
public DbSet<FarmAnimal> FarmAnimals => Set<FarmAnimal>();
public DbSet<Cat> Cats => Set<Cat>();
public DbSet<Dog> Dogs => Set<Dog>();
public DbSet<Human> Humans => Set<Human>();

Lub przy użyciu metody w pliku EntityOnModelCreating:

modelBuilder.Entity<Animal>();
modelBuilder.Entity<Pet>();
modelBuilder.Entity<Cat>();
modelBuilder.Entity<Dog>();
modelBuilder.Entity<FarmAnimal>();
modelBuilder.Entity<Human>();

Ważne

Różni się to od starszego zachowania EF6, w którym typy pochodne mapowanych typów bazowych zostaną automatycznie odnalezione, jeśli znajdują się w tym samym zestawie.

Nie trzeba nic innego robić, aby mapować hierarchię jako TPH, ponieważ jest to strategia domyślna. Jednak począwszy od ef7, funkcja TPH może być jawna przez wywołanie UseTphMappingStrategy podstawowego typu hierarchii:

modelBuilder.Entity<Animal>().UseTphMappingStrategy();

Aby zamiast tego użyć TPT, zmień wartość na UseTptMappingStrategy:

modelBuilder.Entity<Animal>().UseTptMappingStrategy();

Analogicznie, UseTpcMappingStrategy służy do konfigurowania TPC:

modelBuilder.Entity<Animal>().UseTpcMappingStrategy();

W każdym przypadku nazwa tabeli używana dla każdego typu jest pobierana z DbSet nazwy właściwości w DbContextobiekcie lub można skonfigurować przy użyciu ToTable metody konstruktora lub atrybutu [Table] .

Wydajność zapytań TPC

W przypadku zapytań strategia TPC jest ulepszeniem TPT, ponieważ gwarantuje, że informacje dla danego wystąpienia jednostki są zawsze przechowywane w jednej tabeli. Oznacza to, że strategia TPC może być przydatna, gdy zamapowana hierarchia jest duża i ma wiele typów betonowych (zwykle liści), z których każda ma dużą liczbę właściwości i gdzie w większości zapytań jest używany tylko niewielki podzbiór typów.

Język SQL wygenerowany dla trzech prostych zapytań LINQ może służyć do obserwowania, gdzie TPC działa dobrze w porównaniu z TPH i TPT. Te zapytania to:

  1. Zapytanie zwracające jednostki wszystkich typów w hierarchii:

    context.Animals.ToList();
    
  2. Zapytanie zwracające jednostki z podzestawu typów w hierarchii:

    context.Pets.ToList();
    
  3. Zapytanie zwracające tylko jednostki z pojedynczego typu liścia w hierarchii:

    context.Cats.ToList();
    

Zapytania TPH

W przypadku korzystania z funkcji TPH wszystkie trzy zapytania wysyłają zapytanie tylko do jednej tabeli, ale z różnymi filtrami w kolumnie dyskryminującej:

  1. Funkcja TPH SQL zwraca jednostki wszystkich typów w hierarchii:

    SELECT [a].[Id], [a].[Discriminator], [a].[FoodId], [a].[Name], [a].[Species], [a].[Value], [a].[FavoriteAnimalId], [a].[Vet], [a].[EducationLevel], [a].[FavoriteToy]
    FROM [Animals] AS [a]
    
  2. Funkcja TPH SQL zwraca jednostki z podzestawu typów w hierarchii:

    SELECT [a].[Id], [a].[Discriminator], [a].[FoodId], [a].[Name], [a].[Vet], [a].[EducationLevel], [a].[FavoriteToy]
    FROM [Animals] AS [a]
    WHERE [a].[Discriminator] IN (N'Cat', N'Dog')
    
  3. Funkcja TPH SQL zwraca tylko jednostki z pojedynczego typu liścia w hierarchii:

    SELECT [a].[Id], [a].[Discriminator], [a].[FoodId], [a].[Name], [a].[Vet], [a].[EducationLevel]
    FROM [Animals] AS [a]
    WHERE [a].[Discriminator] = N'Cat'
    

Wszystkie te zapytania powinny działać dobrze, szczególnie w przypadku odpowiedniego indeksu bazy danych w kolumnie dyskryminującej.

Zapytania TPT

W przypadku korzystania z TPT wszystkie te zapytania wymagają łączenia wielu tabel, ponieważ dane dla dowolnego konkretnego typu są dzielone między wiele tabel:

  1. Funkcja TPT SQL zwraca jednostki wszystkich typów w hierarchii:

    SELECT [a].[Id], [a].[FoodId], [a].[Name], [f].[Species], [f].[Value], [h].[FavoriteAnimalId], [p].[Vet], [c].[EducationLevel], [d].[FavoriteToy], CASE
        WHEN [d].[Id] IS NOT NULL THEN N'Dog'
        WHEN [c].[Id] IS NOT NULL THEN N'Cat'
        WHEN [h].[Id] IS NOT NULL THEN N'Human'
        WHEN [f].[Id] IS NOT NULL THEN N'FarmAnimal'
    END AS [Discriminator]
    FROM [Animals] AS [a]
    LEFT JOIN [FarmAnimals] AS [f] ON [a].[Id] = [f].[Id]
    LEFT JOIN [Humans] AS [h] ON [a].[Id] = [h].[Id]
    LEFT JOIN [Pets] AS [p] ON [a].[Id] = [p].[Id]
    LEFT JOIN [Cats] AS [c] ON [a].[Id] = [c].[Id]
    LEFT JOIN [Dogs] AS [d] ON [a].[Id] = [d].[Id]
    
  2. TPT SQL zwraca jednostki z podzestawu typów w hierarchii:

    SELECT [a].[Id], [a].[FoodId], [a].[Name], [p].[Vet], [c].[EducationLevel], [d].[FavoriteToy], CASE
        WHEN [d].[Id] IS NOT NULL THEN N'Dog'
        WHEN [c].[Id] IS NOT NULL THEN N'Cat'
    END AS [Discriminator]
    FROM [Animals] AS [a]
    INNER JOIN [Pets] AS [p] ON [a].[Id] = [p].[Id]
    LEFT JOIN [Cats] AS [c] ON [a].[Id] = [c].[Id]
    LEFT JOIN [Dogs] AS [d] ON [a].[Id] = [d].[Id]
    
  3. TPT SQL zwraca tylko jednostki z jednego typu liścia w hierarchii:

    SELECT [a].[Id], [a].[FoodId], [a].[Name], [p].[Vet], [c].[EducationLevel]
    FROM [Animals] AS [a]
    INNER JOIN [Pets] AS [p] ON [a].[Id] = [p].[Id]
    INNER JOIN [Cats] AS [c] ON [a].[Id] = [c].[Id]
    

Uwaga

Program EF Core używa "syntezy dyskryminującej", aby określić, z której tabeli pochodzą dane, a tym samym prawidłowy typ do użycia. Działa to, ponieważ funkcja LEFT JOIN zwraca wartości null dla kolumny identyfikatora zależnego (tabele podrzędne), które nie są poprawnym typem. Więc dla psa, [d].[Id] będzie non-null, a wszystkie pozostałe (betonowe) identyfikatory będą mieć wartość null.

Wszystkie te zapytania mogą mieć problemy z wydajnością spowodowane sprzężeniami tabeli. Dlatego TPT nigdy nie jest dobrym wyborem dla wydajności zapytań.

Zapytania TPC

Funkcja TPC poprawia przepływ pracy dla wszystkich tych zapytań, ponieważ liczba tabel, które należy wykonać zapytania, jest ograniczona. Ponadto wyniki z każdej tabeli są łączone przy użyciu metody UNION ALL, która może być znacznie szybsza niż sprzężenie tabeli, ponieważ nie musi wykonywać żadnych dopasowań między wierszami ani deduplikować wierszy.

  1. Funkcja TPC SQL zwraca jednostki wszystkich typów w hierarchii:

    SELECT [f].[Id], [f].[FoodId], [f].[Name], [f].[Species], [f].[Value], NULL AS [FavoriteAnimalId], NULL AS [Vet], NULL AS [EducationLevel], NULL AS [FavoriteToy], N'FarmAnimal' AS [Discriminator]
    FROM [FarmAnimals] AS [f]
    UNION ALL
    SELECT [h].[Id], [h].[FoodId], [h].[Name], NULL AS [Species], NULL AS [Value], [h].[FavoriteAnimalId], NULL AS [Vet], NULL AS [EducationLevel], NULL AS [FavoriteToy], N'Human' AS [Discriminator]
    FROM [Humans] AS [h]
    UNION ALL
    SELECT [c].[Id], [c].[FoodId], [c].[Name], NULL AS [Species], NULL AS [Value], NULL AS [FavoriteAnimalId], [c].[Vet], [c].[EducationLevel], NULL AS [FavoriteToy], N'Cat' AS [Discriminator]
    FROM [Cats] AS [c]
    UNION ALL
    SELECT [d].[Id], [d].[FoodId], [d].[Name], NULL AS [Species], NULL AS [Value], NULL AS [FavoriteAnimalId], [d].[Vet], NULL AS [EducationLevel], [d].[FavoriteToy], N'Dog' AS [Discriminator]
    FROM [Dogs] AS [d]
    
  2. TPC SQL zwraca jednostki z podzestawu typów w hierarchii:

    SELECT [c].[Id], [c].[FoodId], [c].[Name], [c].[Vet], [c].[EducationLevel], NULL AS [FavoriteToy], N'Cat' AS [Discriminator]
    FROM [Cats] AS [c]
    UNION ALL
    SELECT [d].[Id], [d].[FoodId], [d].[Name], [d].[Vet], NULL AS [EducationLevel], [d].[FavoriteToy], N'Dog' AS [Discriminator]
    FROM [Dogs] AS [d]
    
  3. TPC SQL zwraca tylko jednostki z pojedynczego typu liścia w hierarchii:

    SELECT [c].[Id], [c].[FoodId], [c].[Name], [c].[Vet], [c].[EducationLevel]
    FROM [Cats] AS [c]
    

Mimo że funkcja TPC jest lepsza niż TPT dla wszystkich tych zapytań, zapytania TPH są nadal lepsze podczas zwracania wystąpień wielu typów. Jest to jedna z przyczyn, dla których TPH jest domyślną strategią używaną przez program EF Core.

Jak pokazuje język SQL dla zapytania nr 3, funkcja TPC naprawdę wyróżnia się podczas wykonywania zapytań dotyczących jednostek pojedynczego typu liścia. Zapytanie używa tylko jednej tabeli i nie wymaga filtrowania.

Wstawia i aktualizacje TPC

Funkcja TPC działa również dobrze podczas zapisywania nowej jednostki, ponieważ wymaga to wstawienia tylko jednego wiersza do pojedynczej tabeli. Dotyczy to również TPH. W przypadku TPT wiersze muszą być wstawione do wielu tabel, co będzie działać mniej dobrze.

To samo jest często prawdziwe w przypadku aktualizacji, chociaż w tym przypadku, jeśli wszystkie aktualizowane kolumny znajdują się w tej samej tabeli, nawet w przypadku TPT, różnica może nie być znacząca.

Zagadnienia dotyczące przestrzeni

Zarówno TPT, jak i TPC mogą używać mniej miejsca niż TPH, gdy istnieje wiele podtypów z wieloma właściwościami, które często nie są używane. Dzieje się tak, ponieważ każdy wiersz w tabeli TPH musi przechowywać NULL dla każdej z tych nieużywanych właściwości. W praktyce rzadko występuje problem, ale warto rozważyć przechowywanie dużych ilości danych z tymi cechami.

Napiwek

Jeśli system bazy danych go obsługuje (np. program SQL Server), rozważ użycie kolumn rozrzedzonych dla kolumn TPH, które będą rzadko wypełniane.

Generowanie klucza

Wybrana strategia mapowania dziedziczenia ma konsekwencje dla sposobu generowania i zarządzania wartościami klucza podstawowego. Klucze w TPH są łatwe, ponieważ każde wystąpienie jednostki jest reprezentowane przez jeden wiersz w jednej tabeli. Można użyć dowolnego rodzaju generowania wartości klucza i nie są potrzebne żadne dodatkowe ograniczenia.

W przypadku strategii TPT zawsze istnieje wiersz w tabeli zamapowany na podstawowy typ hierarchii. W tym wierszu można używać dowolnego rodzaju generowania kluczy, a klucze dla innych tabel są połączone z tą tabelą przy użyciu ograniczeń klucza obcego.

Rzeczy są nieco bardziej skomplikowane dla TPC. Najpierw należy pamiętać, że platforma EF Core wymaga, aby wszystkie jednostki w hierarchii miały unikatową wartość klucza, nawet jeśli jednostki mają różne typy. W związku z tym, korzystając z naszego przykładowego modelu, pies nie może mieć tej samej wartości klucza identyfikatora co Kot. Po drugie, w przeciwieństwie do TPT, nie ma wspólnej tabeli, która może działać jako pojedyncze miejsce, w którym żyją wartości kluczy i można je wygenerować. Oznacza to, że nie można użyć prostej Identity kolumny.

W przypadku baz danych obsługujących sekwencje wartości kluczy można wygenerować przy użyciu pojedynczej sekwencji przywoływanej w domyślnym ograniczeniu dla każdej tabeli. Jest to strategia używana w tabelach TPC przedstawionych powyżej, gdzie każda tabela ma następujące elementy:

[Id] int NOT NULL DEFAULT (NEXT VALUE FOR [AnimalSequence])

AnimalSequence to sekwencja bazy danych utworzona przez program EF Core. Ta strategia jest używana domyślnie w przypadku hierarchii TPC podczas korzystania z dostawcy bazy danych EF Core dla programu SQL Server. Dostawcy baz danych dla innych baz danych obsługujących sekwencje powinny mieć podobną wartość domyślną. Inne strategie generowania kluczy, które używają sekwencji, takich jak wzorce Hi-Lo, mogą być również używane z TPC.

Chociaż standardowe kolumny tożsamości nie będą działać z TPC, można użyć kolumn Identity, jeśli każda tabela jest skonfigurowana z odpowiednim inicjatorem i przyrostem, tak aby wartości wygenerowane dla każdej tabeli nigdy nie powodować konfliktu. Przykład:

modelBuilder.Entity<Cat>().ToTable("Cats", tb => tb.Property(e => e.Id).UseIdentityColumn(1, 4));
modelBuilder.Entity<Dog>().ToTable("Dogs", tb => tb.Property(e => e.Id).UseIdentityColumn(2, 4));
modelBuilder.Entity<FarmAnimal>().ToTable("FarmAnimals", tb => tb.Property(e => e.Id).UseIdentityColumn(3, 4));
modelBuilder.Entity<Human>().ToTable("Humans", tb => tb.Property(e => e.Id).UseIdentityColumn(4, 4));

SqLite nie obsługuje sekwencji ani inkrementacji tożsamości, dlatego generowanie wartości klucza całkowitego nie jest obsługiwane w przypadku używania biblioteki SQLite ze strategią TPC. Jednak generowanie po stronie klienta lub globalnie unikatowe klucze — na przykład klucze GUID — są obsługiwane w dowolnej bazie danych, w tym sqlite.

Ograniczenia klucza obcego

Strategia mapowania TPC tworzy zdenormalizowany schemat SQL — jest to jeden z powodów, dla których niektórzy puryści bazy danych są przeciwni. Rozważmy na przykład kolumnę FavoriteAnimalIdklucza obcego . Wartość w tej kolumnie musi być zgodna z wartością klucza podstawowego niektórych zwierząt. Można to wymusić w bazie danych z prostym ograniczeniem FK podczas korzystania z TPH lub TPT. Przykład:

CONSTRAINT [FK_Animals_Animals_FavoriteAnimalId] FOREIGN KEY ([FavoriteAnimalId]) REFERENCES [Animals] ([Id])

Jednak w przypadku korzystania z TPC klucz podstawowy dla zwierzęcia jest przechowywany w tabeli dla konkretnego typu tego zwierzęcia. Na przykład klucz podstawowy kota jest przechowywany w Cats.Id kolumnie, podczas gdy klucz podstawowy psa jest przechowywany w Dogs.Id kolumnie itd. Oznacza to, że nie można utworzyć ograniczenia FK dla tej relacji.

W praktyce nie jest to problem, o ile aplikacja nie próbuje wstawić nieprawidłowych danych. Jeśli na przykład wszystkie dane są wstawione przez program EF Core i używają nawigacji do powiązania jednostek, gwarantowana jest, że kolumna FK będzie zawierać prawidłową wartość PK przez cały czas.

Podsumowanie i wskazówki

Podsumowując, TPC jest dobrą strategią mapowania, która ma być używana, gdy kod będzie w większości wykonywać zapytania dotyczące jednostek pojedynczego typu liścia. Wynika to z tego, że wymagania dotyczące magazynu są mniejsze i nie ma dyskryminującej kolumny, która może wymagać indeksu. Wstawianie i aktualizacje są również wydajne.

To powiedziawszy, TPH jest zwykle w porządku dla większości aplikacji i jest dobrym ustawieniem domyślnym dla szerokiej gamy scenariuszy, więc nie dodawaj złożoności TPC, jeśli nie potrzebujesz. W szczególności jeśli kod będzie w większości wykonywać zapytania dotyczące jednostek wielu typów, takich jak pisanie zapytań względem typu podstawowego, pochylić się w kierunku TPH przez TPC.

Należy użyć TPT tylko wtedy, gdy jest to ograniczone przez czynniki zewnętrzne.

Niestandardowe szablony inżynierii odwrotnej

Teraz można dostosować kod szkieletowy podczas odtwarzania modelu EF z bazy danych. Rozpocznij od dodania domyślnych szablonów do projektu:

dotnet new install Microsoft.EntityFrameworkCore.Templates
dotnet new ef-templates

Następnie szablony mogą być dostosowywane i będą automatycznie używane przez dotnet ef dbcontext scaffold program i Scaffold-DbContext.

Aby uzyskać więcej informacji, zobacz Niestandardowe szablony inżynierii odwrotnej.

Napiwek

Zespół ef team zademonstrował i omówił szczegółowo szablony inżynierii odwrotnej w odcinku standupu społeczności danych platformy .NET. Podobnie jak w przypadku wszystkich odcinków standup społeczności, możesz obejrzeć odcinek T4 szablonów teraz na YouTube.

Konwencje tworzenia modeli

Program EF Core używa metadanych "model" do opisania sposobu mapowania typów jednostek aplikacji na bazową bazę danych. Ten model jest tworzony przy użyciu zestawu około 60 "konwencji". Model utworzony za pomocą konwencji można następnie dostosować przy użyciu atrybutów mapowania (np. "adnotacje danych") i/lub wywołań interfejsuDbModelBuilderAPI w programie OnModelCreating.

Począwszy od ef7, aplikacje mogą teraz usuwać lub zastępować dowolną z tych konwencji, a także dodawać nowe konwencje. Konwencje tworzenia modeli to zaawansowany sposób kontrolowania konfiguracji modelu, ale może być złożony i trudny do uzyskania właściwego rozwiązania. W wielu przypadkach można użyć istniejącej konfiguracji modelu przed konwencją, aby łatwo określić wspólną konfigurację właściwości i typów.

Zmiany konwencji używanych przez element DbContext są wprowadzane przez zastąpienie DbContext.ConfigureConventions metody . Przykład:

protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
{
    configurationBuilder.Conventions.Remove(typeof(ForeignKeyIndexConvention));
}

Napiwek

Aby znaleźć wszystkie wbudowane konwencje tworzenia modeli, poszukaj każdej klasy, która implementuje IConvention interfejs.

Napiwek

Pokazany tutaj kod pochodzi z pliku ModelBuildingConventionsSample.cs.

Usuwanie istniejącej konwencji

Czasami jedna z wbudowanych konwencji może nie być odpowiednia dla aplikacji, w tym przypadku można ją usunąć.

Przykład: nie twórz indeksów dla kolumn kluczy obcych

Zwykle warto utworzyć indeksy dla kolumn klucza obcego (FK), dlatego istnieje wbudowana konwencja dla tego: ForeignKeyIndexConvention. Patrząc na widok debugowania modelu dla Post typu jednostki z relacjami z Blog i Author, możemy zobaczyć, że dwa indeksy są tworzone — jeden dla BlogId klucza FK, a drugi dla AuthorId klucza FK.

  EntityType: Post
    Properties:
      Id (int) Required PK AfterSave:Throw ValueGenerated.OnAdd
      AuthorId (no field, int?) Shadow FK Index
      BlogId (no field, int) Shadow Required FK Index
    Navigations:
      Author (Author) ToPrincipal Author Inverse: Posts
      Blog (Blog) ToPrincipal Blog Inverse: Posts
    Keys:
      Id PK
    Foreign keys:
      Post {'AuthorId'} -> Author {'Id'} ToDependent: Posts ToPrincipal: Author ClientSetNull
      Post {'BlogId'} -> Blog {'Id'} ToDependent: Posts ToPrincipal: Blog Cascade
    Indexes:
      AuthorId
      BlogId

Jednak indeksy mają narzut i, jak zapytano tutaj, może nie zawsze być odpowiednie do utworzenia ich dla wszystkich kolumn FK. Aby to osiągnąć, ForeignKeyIndexConvention można go usunąć podczas kompilowania modelu:

protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
{
    configurationBuilder.Conventions.Remove(typeof(ForeignKeyIndexConvention));
}

Patrząc na widok debugowania modelu na Post razie, widzimy, że indeksy na zestawach FKs nie zostały utworzone:

  EntityType: Post
    Properties:
      Id (int) Required PK AfterSave:Throw ValueGenerated.OnAdd
      AuthorId (no field, int?) Shadow FK
      BlogId (no field, int) Shadow Required FK
    Navigations:
      Author (Author) ToPrincipal Author Inverse: Posts
      Blog (Blog) ToPrincipal Blog Inverse: Posts
    Keys:
      Id PK
    Foreign keys:
      Post {'AuthorId'} -> Author {'Id'} ToDependent: Posts ToPrincipal: Author ClientSetNull
      Post {'BlogId'} -> Blog {'Id'} ToDependent: Posts ToPrincipal: Blog Cascade

W razie potrzeby indeksy mogą być nadal jawnie tworzone dla kolumn kluczy obcych przy użyciu elementu IndexAttribute:

[Index("BlogId")]
public class Post
{
    // ...
}

Lub z konfiguracją w pliku OnModelCreating:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Post>(entityTypeBuilder => entityTypeBuilder.HasIndex("BlogId"));
}

Ponowne przyjrzenie się typowi Post jednostki zawiera BlogId indeks, ale nie AuthorId indeks:

  EntityType: Post
    Properties:
      Id (int) Required PK AfterSave:Throw ValueGenerated.OnAdd
      AuthorId (no field, int?) Shadow FK
      BlogId (no field, int) Shadow Required FK Index
    Navigations:
      Author (Author) ToPrincipal Author Inverse: Posts
      Blog (Blog) ToPrincipal Blog Inverse: Posts
    Keys:
      Id PK
    Foreign keys:
      Post {'AuthorId'} -> Author {'Id'} ToDependent: Posts ToPrincipal: Author ClientSetNull
      Post {'BlogId'} -> Blog {'Id'} ToDependent: Posts ToPrincipal: Blog Cascade
    Indexes:
      BlogId

Napiwek

Jeśli model nie używa atrybutów mapowania (adnotacji danych) do konfiguracji, wszystkie konwencje kończące AttributeConvention się można bezpiecznie usunąć, aby przyspieszyć tworzenie modelu.

Dodawanie nowej konwencji

Usunięcie istniejących konwencji jest początkiem, ale co z dodaniem całkowicie nowych konwencji tworzenia modelu? Program EF7 obsługuje to również!

Przykład: ograniczenie długości właściwości dyskryminujących

Strategia mapowania dziedziczenia tabeli na hierarchię wymaga dyskryminującej kolumny w celu określenia, który typ jest reprezentowany w danym wierszu. Domyślnie program EF używa niezwiązanej kolumny ciągu dla dyskryminującego, co gwarantuje, że będzie działać dla dowolnej długości dyskryminującej. Ograniczenie maksymalnej długości ciągów dyskryminujących może jednak zwiększyć wydajność magazynowania i zapytań. Utwórzmy nową konwencję, która to zrobi.

Konwencje tworzenia modeli platformy EF Core są wyzwalane na podstawie zmian wprowadzonych w modelu podczas jego tworzenia. Dzięki temu model jest aktualny w miarę tworzenia jawnej konfiguracji, są stosowane atrybuty mapowania i są uruchamiane inne konwencje. Aby wziąć udział w tym procesie, każda konwencja implementuje jeden lub więcej interfejsów, które określają, kiedy konwencja zostanie wyzwolona. Na przykład konwencja implementowana zostanie wyzwolona IEntityTypeAddedConvention za każdym razem, gdy nowy typ jednostki zostanie dodany do modelu. Podobnie konwencja implementowana zarówno, IForeignKeyAddedConvention jak i IKeyAddedConvention zostanie wyzwolona za każdym razem, gdy do modelu zostanie dodany klucz lub klucz obcy.

Znajomość interfejsów do zaimplementowania może być trudna, ponieważ konfiguracja w danym momencie może zostać zmieniona lub usunięta w późniejszym momencie. Na przykład klucz może zostać utworzony zgodnie z konwencją, ale później zastąpiony podczas jawnego konfigurowania innego klucza.

Zróbmy to nieco bardziej konkretnie, podejmując pierwszą próbę wdrożenia konwencji o długości dyskryminującej:

public class DiscriminatorLengthConvention1 : IEntityTypeBaseTypeChangedConvention
{
    public void ProcessEntityTypeBaseTypeChanged(
        IConventionEntityTypeBuilder entityTypeBuilder,
        IConventionEntityType? newBaseType,
        IConventionEntityType? oldBaseType,
        IConventionContext<IConventionEntityType> context)
    {
        var discriminatorProperty = entityTypeBuilder.Metadata.FindDiscriminatorProperty();
        if (discriminatorProperty != null
            && discriminatorProperty.ClrType == typeof(string))
        {
            discriminatorProperty.Builder.HasMaxLength(24);
        }
    }
}

Ta konwencja implementuje IEntityTypeBaseTypeChangedConventionelement , co oznacza, że zostanie wyzwolony przy każdej zmianie zamapowanej hierarchii dziedziczenia dla typu jednostki. Następnie konwencja znajduje i konfiguruje właściwość dyskryminującą ciąg dla hierarchii.

Ta konwencja jest następnie używana przez wywołanie metody Add w pliku ConfigureConventions:

protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
{
    configurationBuilder.Conventions.Add(_ =>  new DiscriminatorLengthConvention1());
}

Napiwek

Zamiast bezpośrednio dodawać wystąpienie konwencji, Add metoda akceptuje fabrykę do tworzenia wystąpień konwencji. Dzięki temu konwencja może używać zależności od wewnętrznego dostawcy usług EF Core. Ponieważ ta konwencja nie ma zależności, parametr dostawcy usług ma nazwę _, wskazując, że nigdy nie jest używany.

Utworzenie modelu i przyjrzenie się typowi Post jednostki pokazuje, że ta właściwość działa — właściwość dyskryminująca jest teraz skonfigurowana do maksymalnej długości 24:

 Discriminator (no field, string) Shadow Required AfterSave:Throw MaxLength(24)

Ale co się stanie, jeśli teraz jawnie skonfigurujemy inną właściwość dyskryminującą? Przykład:

modelBuilder.Entity<Post>()
    .HasDiscriminator<string>("PostTypeDiscriminator")
    .HasValue<Post>("Post")
    .HasValue<FeaturedPost>("Featured");

Patrząc na widok debugowania modelu, uważamy, że długość dyskryminująca nie jest już skonfigurowana.

 PostTypeDiscriminator (no field, string) Shadow Required AfterSave:Throw

Wynika to z faktu, że właściwość dyskryminująca skonfigurowana w naszej konwencji została później usunięta po dodaniu niestandardowego dyskryminatora. Moglibyśmy spróbować rozwiązać ten problem, wdrażając inny interfejs w naszej konwencji, aby reagować na zmiany dyskryminujące, ale ustalenie, który interfejs do zaimplementowania nie jest łatwy.

Na szczęście istnieje inny sposób podejścia, który znacznie ułatwia kwestie. Wiele czasu nie ma znaczenia, jak wygląda model podczas kompilowania, o ile ostateczny model jest poprawny. Ponadto konfiguracja, którą chcemy zastosować, często nie musi wyzwalać innych konwencji, aby reagować. W związku z tym nasza konwencja może implementować IModelFinalizingConvention. Konwencje finalizacji modelu są uruchamiane po zakończeniu wszystkich innych kompilacji modeli, a więc mają dostęp do końcowego stanu modelu. Konwencja finalizowania modelu zwykle iteruje cały model konfigurujący elementy modelu w miarę jego działania. W tym przypadku znajdziemy więc każdy dyskryminator w modelu i skonfigurujemy go:

public class DiscriminatorLengthConvention2 : IModelFinalizingConvention
{
    public void ProcessModelFinalizing(IConventionModelBuilder modelBuilder, IConventionContext<IConventionModelBuilder> context)
    {
        foreach (var entityType in modelBuilder.Metadata.GetEntityTypes()
                     .Where(entityType => entityType.BaseType == null))
        {
            var discriminatorProperty = entityType.FindDiscriminatorProperty();
            if (discriminatorProperty != null
                && discriminatorProperty.ClrType == typeof(string))
            {
                discriminatorProperty.Builder.HasMaxLength(24);
            }
        }
    }
}

Po utworzeniu modelu z tą nową konwencją okazuje się, że długość dyskryminująca jest teraz poprawnie skonfigurowana, mimo że została ona dostosowana:

PostTypeDiscriminator (no field, string) Shadow Required AfterSave:Throw MaxLength(24)

Po prostu dla zabawy, przejdźmy o krok dalej i skonfigurujmy maksymalną długość najdłuższej wartości dyskryminującej.

public class DiscriminatorLengthConvention3 : IModelFinalizingConvention
{
    public void ProcessModelFinalizing(IConventionModelBuilder modelBuilder, IConventionContext<IConventionModelBuilder> context)
    {
        foreach (var entityType in modelBuilder.Metadata.GetEntityTypes()
                     .Where(entityType => entityType.BaseType == null))
        {
            var discriminatorProperty = entityType.FindDiscriminatorProperty();
            if (discriminatorProperty != null
                && discriminatorProperty.ClrType == typeof(string))
            {
                var maxDiscriminatorValueLength =
                    entityType.GetDerivedTypesInclusive().Select(e => ((string)e.GetDiscriminatorValue()!).Length).Max();

                discriminatorProperty.Builder.HasMaxLength(maxDiscriminatorValueLength);
            }
        }
    }
}

Teraz maksymalna długość kolumny dyskryminującej wynosi 8, czyli długość "Polecane", najdłuższa wartość dyskryminująca w użyciu.

PostTypeDiscriminator (no field, string) Shadow Required AfterSave:Throw MaxLength(8)

Napiwek

Być może zastanawiasz się, czy konwencja powinna również utworzyć indeks dla kolumny dyskryminującej. Istnieje dyskusja na temat tego tematu w witrynie GitHub. Krótka odpowiedź polega na tym, że czasami indeks może być przydatny, ale przez większość czasu prawdopodobnie nie będzie. Dlatego najlepiej jest utworzyć odpowiednie indeksy w razie potrzeby, a nie mieć konwencji, aby to robić zawsze. Ale jeśli nie zgadzasz się z tym, powyższe konwencje można łatwo zmodyfikować w celu utworzenia indeksu.

Przykład: domyślna długość wszystkich właściwości ciągu

Przyjrzyjmy się innemu przykładowi, w którym można użyć konwencji finalizowania — tym razem ustawiając domyślną maksymalną długość dowolnej właściwości ciągu, która jest żądana w usłudze GitHub. Konwencja wygląda podobnie do poprzedniego przykładu:

public class MaxStringLengthConvention : IModelFinalizingConvention
{
    public void ProcessModelFinalizing(IConventionModelBuilder modelBuilder, IConventionContext<IConventionModelBuilder> context)
    {
        foreach (var property in modelBuilder.Metadata.GetEntityTypes()
                     .SelectMany(
                         entityType => entityType.GetDeclaredProperties()
                             .Where(
                                 property => property.ClrType == typeof(string))))
        {
            property.Builder.HasMaxLength(512);
        }
    }
}

Ta konwencja jest dość prosta. Znajduje on każdą właściwość ciągu w modelu i ustawia maksymalną długość na 512. Patrząc w widoku debugowania we właściwościach Postelementu , widzimy, że wszystkie właściwości ciągu mają teraz maksymalną długość 512.

EntityType: Post
  Properties:
    Id (int) Required PK AfterSave:Throw ValueGenerated.OnAdd
    AuthorId (no field, int?) Shadow FK Index
    BlogId (no field, int) Shadow Required FK Index
    Content (string) Required MaxLength(512)
    Discriminator (no field, string) Shadow Required AfterSave:Throw MaxLength(512)
    PublishedOn (DateTime) Required
    Title (string) Required MaxLength(512)

Content Ale właściwość powinna prawdopodobnie zezwalać na więcej niż 512 znaków, lub wszystkie nasze posty będą dość krótkie! Można to zrobić bez zmiany konwencji, jawnie konfigurując maksymalną długość tylko dla tej właściwości przy użyciu atrybutu mapowania:

[MaxLength(4000)]
public string Content { get; set; }

Lub za pomocą kodu w pliku OnModelCreating:

modelBuilder.Entity<Post>()
    .Property(post => post.Content)
    .HasMaxLength(4000);

Teraz wszystkie właściwości mają maksymalną długość 512, z wyjątkiem Content jawnie skonfigurowanej z 4000:

EntityType: Post
  Properties:
    Id (int) Required PK AfterSave:Throw ValueGenerated.OnAdd
    AuthorId (no field, int?) Shadow FK Index
    BlogId (no field, int) Shadow Required FK Index
    Content (string) Required MaxLength(4000)
    Discriminator (no field, string) Shadow Required AfterSave:Throw MaxLength(512)
    PublishedOn (DateTime) Required
    Title (string) Required MaxLength(512)

Dlaczego więc nasza konwencja nie zastąpi jawnie skonfigurowanej maksymalnej długości? Odpowiedź brzmi, że program EF Core śledzi, jak każda konfiguracja została wykonana. Jest to reprezentowane przez wyliczenie ConfigurationSource . Różne rodzaje konfiguracji to:

  • Explicit: Element modelu został jawnie skonfigurowany w programie OnModelCreating
  • DataAnnotation: Element modelu został skonfigurowany przy użyciu atrybutu mapowania (adnotacji danych) w typie CLR
  • Convention: Element modelu został skonfigurowany zgodnie z konwencją tworzenia modelu

Konwencje nigdy nie zastępują konfiguracji oznaczonej jako DataAnnotation lub Explicit. Jest to osiągane przy użyciu "konstruktora konwencji", IConventionPropertyBuilderna przykład , który jest uzyskiwany z Builder właściwości . Przykład:

property.Builder.HasMaxLength(512);

Wywołanie metody HasMaxLength na konstruktorze konwencji spowoduje ustawienie maksymalnej długości tylko wtedy, gdy nie został jeszcze skonfigurowany przez atrybut mapowania lub w .OnModelCreating

Metody konstruktora takie jak ten mają również drugi parametr: fromDataAnnotation. Ustaw tę wartość na true wartość , jeśli konwencja tworzy konfigurację w imieniu atrybutu mapowania. Przykład:

property.Builder.HasMaxLength(512, fromDataAnnotation: true);

Spowoduje to ustawienie ConfigurationSource wartości na DataAnnotation, co oznacza, że wartość może być teraz zastępowana przez jawne mapowanie na OnModelCreating, ale nie przez konwencje atrybutów niemapowania.

Na koniec, zanim opuścimy ten przykład, co się stanie, jeśli używamy obu elementów MaxStringLengthConvention i DiscriminatorLengthConvention3 w tym samym czasie? Odpowiedź polega na tym, że zależy od kolejności ich dodania, ponieważ konwencje finalizacji modelu są uruchamiane w kolejności ich dodawania. Więc jeśli MaxStringLengthConvention zostanie dodany ostatnio, będzie on uruchamiany jako ostatni, i ustawi maksymalną długość właściwości dyskryminującej na 512. W związku z tym w tym przypadku lepiej jest dodać DiscriminatorLengthConvention3 ostatnio, aby można było zastąpić domyślną maksymalną długość tylko właściwości dyskryminujących, pozostawiając wszystkie inne właściwości ciągu jako 512.

Zastępowanie istniejącej konwencji

Czasami zamiast całkowicie usuwać istniejącą konwencję, zamiast tego chcemy zastąpić ją konwencją, która zasadniczo robi to samo, ale ze zmienionym zachowaniem. Jest to przydatne, ponieważ istniejąca konwencja będzie już implementować interfejsy, których potrzebuje, tak aby była odpowiednio wyzwalana.

Przykład: mapowanie właściwości opt-in

Program EF Core mapuje wszystkie publiczne właściwości odczytu i zapisu według konwencji. Może to nie być odpowiednie dla sposobu definiowania typów jednostek. Aby to zmienić, możemy zastąpić PropertyDiscoveryConvention element własnym implementacją, która nie mapuje żadnej właściwości, chyba że jest jawnie zamapowana lub OnModelCreating oznaczona nowym atrybutem o nazwie Persist:

[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)]
public sealed class PersistAttribute : Attribute
{
}

Oto nowa konwencja:

public class AttributeBasedPropertyDiscoveryConvention : PropertyDiscoveryConvention
{
    public AttributeBasedPropertyDiscoveryConvention(ProviderConventionSetBuilderDependencies dependencies)
        : base(dependencies)
    {
    }

    public override void ProcessEntityTypeAdded(
        IConventionEntityTypeBuilder entityTypeBuilder,
        IConventionContext<IConventionEntityTypeBuilder> context)
        => Process(entityTypeBuilder);

    public override void ProcessEntityTypeBaseTypeChanged(
        IConventionEntityTypeBuilder entityTypeBuilder,
        IConventionEntityType? newBaseType,
        IConventionEntityType? oldBaseType,
        IConventionContext<IConventionEntityType> context)
    {
        if ((newBaseType == null
             || oldBaseType != null)
            && entityTypeBuilder.Metadata.BaseType == newBaseType)
        {
            Process(entityTypeBuilder);
        }
    }

    private void Process(IConventionEntityTypeBuilder entityTypeBuilder)
    {
        foreach (var memberInfo in GetRuntimeMembers())
        {
            if (Attribute.IsDefined(memberInfo, typeof(PersistAttribute), inherit: true))
            {
                entityTypeBuilder.Property(memberInfo);
            }
            else if (memberInfo is PropertyInfo propertyInfo
                     && Dependencies.TypeMappingSource.FindMapping(propertyInfo) != null)
            {
                entityTypeBuilder.Ignore(propertyInfo.Name);
            }
        }

        IEnumerable<MemberInfo> GetRuntimeMembers()
        {
            var clrType = entityTypeBuilder.Metadata.ClrType;

            foreach (var property in clrType.GetRuntimeProperties()
                         .Where(p => p.GetMethod != null && !p.GetMethod.IsStatic))
            {
                yield return property;
            }

            foreach (var property in clrType.GetRuntimeFields())
            {
                yield return property;
            }
        }
    }
}

Napiwek

Podczas zastępowania wbudowanej konwencji nowa implementacja konwencji powinna dziedziczyć z istniejącej klasy konwencji. Należy pamiętać, że niektóre konwencje mają implementacje specyficzne dla relacyjnych lub dostawcy, w tym przypadku nowa implementacja konwencji powinna dziedziczyć z najbardziej konkretnej istniejącej klasy konwencji dla dostawcy bazy danych w użyciu.

Konwencja jest następnie rejestrowana przy użyciu metody w pliku ReplaceConfigureConventions:

protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
{
    configurationBuilder.Conventions.Replace<PropertyDiscoveryConvention>(
        serviceProvider => new AttributeBasedPropertyDiscoveryConvention(
            serviceProvider.GetRequiredService<ProviderConventionSetBuilderDependencies>()));
}

Napiwek

Jest to przypadek, w którym istniejąca konwencja ma zależności reprezentowane przez ProviderConventionSetBuilderDependencies obiekt zależności. Są one uzyskiwane od wewnętrznego dostawcy usług przy użyciu GetRequiredService i przekazywane do konstruktora konwencji.

Ta konwencja działa przez pobranie wszystkich czytelnych właściwości i pól z danego typu jednostki. Jeśli element członkowski jest przypisywany za pomocą [Persist]elementu , jest mapowany przez wywołanie metody :

entityTypeBuilder.Property(memberInfo);

Z drugiej strony, jeśli element członkowski jest właściwością, która w przeciwnym razie zostałaby zamapowana, zostanie wykluczona z modelu przy użyciu:

entityTypeBuilder.Ignore(propertyInfo.Name);

Należy zauważyć, że ta konwencja umożliwia mapowanie pól (oprócz właściwości), o ile są one oznaczone znakiem [Persist]. Oznacza to, że możemy używać pól prywatnych jako ukrytych kluczy w modelu.

Rozważmy na przykład następujące typy jednostek:

public class LaundryBasket
{
    [Persist]
    [Key]
    private readonly int _id;

    [Persist]
    public int TenantId { get; init; }

    public bool IsClean { get; set; }

    public List<Garment> Garments { get; } = new();
}

public class Garment
{
    public Garment(string name, string color)
    {
        Name = name;
        Color = color;
    }

    [Persist]
    [Key]
    private readonly int _id;

    [Persist]
    public int TenantId { get; init; }

    [Persist]
    public string Name { get; }

    [Persist]
    public string Color { get; }

    public bool IsClean { get; set; }

    public LaundryBasket? Basket { get; set; }
}

Model utworzony na podstawie tych typów jednostek to:

Model:
  EntityType: Garment
    Properties:
      _id (_id, int) Required PK AfterSave:Throw ValueGenerated.OnAdd
      Basket_id (no field, int?) Shadow FK Index
      Color (string) Required
      Name (string) Required
      TenantId (int) Required
    Navigations:
      Basket (LaundryBasket) ToPrincipal LaundryBasket Inverse: Garments
    Keys:
      _id PK
    Foreign keys:
      Garment {'Basket_id'} -> LaundryBasket {'_id'} ToDependent: Garments ToPrincipal: Basket ClientSetNull
    Indexes:
      Basket_id
  EntityType: LaundryBasket
    Properties:
      _id (_id, int) Required PK AfterSave:Throw ValueGenerated.OnAdd
      TenantId (int) Required
    Navigations:
      Garments (List<Garment>) Collection ToDependent Garment Inverse: Basket
    Keys:
      _id PK

Należy zauważyć, że normalnie, zostałyby zamapowane, IsClean ale ponieważ nie jest on oznaczony [Perist] (prawdopodobnie ponieważ czystość nie jest trwałą właściwością prania), jest teraz traktowany jako właściwość niemapowana.

Napiwek

Nie można zaimplementować tej konwencji jako konwencji finalizowania modelu, ponieważ mapowanie właściwości wyzwala wiele innych konwencji do uruchomienia w celu dalszego konfigurowania zamapowanej właściwości.

Mapowanie procedury składowanej

Domyślnie program EF Core generuje polecenia wstawiania, aktualizowania i usuwania, które działają bezpośrednio z tabelami lub widokami aktualizowalnymi. Program EF7 wprowadza obsługę mapowania tych poleceń na procedury składowane.

Napiwek

Program EF Core zawsze obsługiwał wykonywanie zapytań za pośrednictwem procedur składowanych. Nowa obsługa w programie EF7 jest jawnie używana w procedurach składowanych w przypadku wstawiania, aktualizacji i usuwania.

Ważne

Obsługa mapowania procedur składowanych nie oznacza, że zalecane są procedury składowane.

Procedury składowane są mapowane przy OnModelCreating użyciu metod InsertUsingStoredProcedure, UpdateUsingStoredProcedurei DeleteUsingStoredProcedure. Aby na przykład zamapować procedury składowane dla Person typu jednostki:

modelBuilder.Entity<Person>()
    .InsertUsingStoredProcedure(
        "People_Insert",
        storedProcedureBuilder =>
        {
            storedProcedureBuilder.HasParameter(a => a.Name);
            storedProcedureBuilder.HasResultColumn(a => a.Id);
        })
    .UpdateUsingStoredProcedure(
        "People_Update",
        storedProcedureBuilder =>
        {
            storedProcedureBuilder.HasOriginalValueParameter(person => person.Id);
            storedProcedureBuilder.HasOriginalValueParameter(person => person.Name);
            storedProcedureBuilder.HasParameter(person => person.Name);
            storedProcedureBuilder.HasRowsAffectedResultColumn();
        })
    .DeleteUsingStoredProcedure(
        "People_Delete",
        storedProcedureBuilder =>
        {
            storedProcedureBuilder.HasOriginalValueParameter(person => person.Id);
            storedProcedureBuilder.HasOriginalValueParameter(person => person.Name);
            storedProcedureBuilder.HasRowsAffectedResultColumn();
        });

Ta konfiguracja jest mapowana na następujące procedury składowane podczas korzystania z programu SQL Server:

W przypadku wstawiania

CREATE PROCEDURE [dbo].[People_Insert]
    @Name [nvarchar](max)
AS
BEGIN
      INSERT INTO [People] ([Name])
      OUTPUT INSERTED.[Id]
      VALUES (@Name);
END

W przypadku aktualizacji

CREATE PROCEDURE [dbo].[People_Update]
    @Id [int],
    @Name_Original [nvarchar](max),
    @Name [nvarchar](max)
AS
BEGIN
    UPDATE [People] SET [Name] = @Name
    WHERE [Id] = @Id AND [Name] = @Name_Original
    SELECT @@ROWCOUNT
END

W przypadku usuwania

CREATE PROCEDURE [dbo].[People_Delete]
    @Id [int],
    @Name_Original [nvarchar](max)
AS
BEGIN
    DELETE FROM [People]
    OUTPUT 1
    WHERE [Id] = @Id AND [Name] = @Name_Original;
END

Napiwek

Procedury składowane nie muszą być używane dla każdego typu w modelu ani dla wszystkich operacji na danym typie. Jeśli na przykład określono tylko DeleteUsingStoredProcedure dla danego typu, program EF Core wygeneruje program SQL normalnie dla operacji wstawiania i aktualizacji i używa tylko procedury składowanej do usuwania.

Pierwszym argumentem przekazywanym do każdej metody jest nazwa procedury składowanej. Można to pominąć, w takim przypadku program EF Core będzie używać nazwy tabeli dołączonej do "_Insert", "_Update" lub "_Delete". Dlatego w powyższym przykładzie, ponieważ tabela nosi nazwę "Osoby", nazwy procedur składowanej można usunąć bez zmian w funkcjonalności.

Drugi argument to konstruktor służący do konfigurowania danych wejściowych i wyjściowych procedury składowanej, w tym parametrów, zwracanych wartości i kolumn wyników.

Parametry

Parametry należy dodać do konstruktora w takiej samej kolejności, jak w definicji procedury składowanej.

Uwaga

Parametry mogą być nazwane, ale program EF Core zawsze wywołuje procedury składowane przy użyciu argumentów pozycyjnych, a nie nazwanych argumentów. Zagłosuj na ustawienie Zezwalaj na konfigurowanie mapowania sproc w celu użycia nazw parametrów do wywołania , jeśli wywołanie według nazwy jest czymś, co cię interesuje.

Pierwszy argument dla każdej metody konstruktora parametrów określa właściwość w modelu, do którego jest powiązany parametr. Może to być wyrażenie lambda:

storedProcedureBuilder.HasParameter(a => a.Name);

Lub ciąg, który jest szczególnie przydatny podczas mapowania właściwości cienia:

storedProcedureBuilder.HasParameter("Name");

Parametry są domyślnie skonfigurowane dla "danych wejściowych". Parametry "Output" lub "input/output" można skonfigurować przy użyciu konstruktora zagnieżdżonego. Przykład:

storedProcedureBuilder.HasParameter(
    document => document.RetrievedOn, 
    parameterBuilder => parameterBuilder.IsOutput());

Istnieją trzy różne metody konstruktora dla różnych odmian parametrów:

  • HasParameter określa normalny parametr powiązany z bieżącą wartością danej właściwości.
  • HasOriginalValueParameter określa parametr powiązany z oryginalną wartością danej właściwości. Oryginalna wartość to wartość, którą właściwość miała podczas wykonywania zapytania z bazy danych, jeśli jest znana. Jeśli ta wartość nie jest znana, zamiast tego zostanie użyta bieżąca wartość. Oryginalne parametry wartości są przydatne w przypadku tokenów współbieżności.
  • HasRowsAffectedParameter określa parametr używany do zwracania liczby wierszy, których dotyczy procedura składowana.

Napiwek

Oryginalne parametry wartości muszą być używane dla wartości klucza w procedurach składowanych "update" i "delete". Dzięki temu prawidłowy wiersz zostanie zaktualizowany w przyszłych wersjach programu EF Core, które obsługują modyfikowalne wartości kluczy.

Zwracanie wartości

Program EF Core obsługuje trzy mechanizmy zwracania wartości z procedur składowanych:

  • Parametry wyjściowe, jak pokazano powyżej.
  • Kolumny wyników określone przy użyciu metody konstruktora HasResultColumn .
  • Wartość zwracana, która jest ograniczona do zwracania liczby wierszy, których dotyczy problem, i jest określona przy użyciu metody konstruktora HasRowsAffectedReturnValue .

Wartości zwracane z procedur składowanych są często używane dla wygenerowanych, domyślnych lub obliczonych wartości, takich jak klucz Identity lub obliczona kolumna. Na przykład następująca konfiguracja określa cztery kolumny wyników:

entityTypeBuilder.InsertUsingStoredProcedure(
        storedProcedureBuilder =>
        {
            storedProcedureBuilder.HasParameter(document => document.Title);
            storedProcedureBuilder.HasResultColumn(document => document.Id);
            storedProcedureBuilder.HasResultColumn(document => document.FirstRecordedOn);
            storedProcedureBuilder.HasResultColumn(document => document.RetrievedOn);
            storedProcedureBuilder.HasResultColumn(document => document.RowVersion);
        });

Są one używane do zwracania:

  • Wygenerowana wartość klucza dla Id właściwości .
  • Wartość domyślna wygenerowana przez bazę danych dla FirstRecordedOn właściwości .
  • Obliczona wartość wygenerowana przez bazę danych dla RetrievedOn właściwości .
  • Automatycznie wygenerowany rowversion token współbieżności dla RowVersion właściwości .

Ta konfiguracja jest mapowana na następującą procedurę składowaną podczas korzystania z programu SQL Server:

CREATE PROCEDURE [dbo].[Documents_Insert]
    @Title [nvarchar](max)
AS
BEGIN
    INSERT INTO [Documents] ([Title])
    OUTPUT INSERTED.[Id], INSERTED.[FirstRecordedOn], INSERTED.[RetrievedOn], INSERTED.[RowVersion]
    VALUES (@Title);
END

Optymistyczna współbieżność

Optymistyczna współbieżność działa tak samo jak w przypadku procedur składowanych, jak w przypadku braku. Procedura składowana powinna:

  • Użyj tokenu współbieżności w WHERE klauzuli , aby upewnić się, że wiersz jest aktualizowany tylko wtedy, gdy ma prawidłowy token. Wartość używana dla tokenu współbieżności jest zazwyczaj, ale nie musi być oryginalną wartością właściwości tokenu współbieżności.
  • Zwróć liczbę wierszy, których dotyczy problem, aby program EF Core mógł porównać go z oczekiwaną liczbą wierszy, których dotyczy problem, i zgłosić DbUpdateConcurrencyException wartość, jeśli wartości nie są zgodne.

Na przykład następująca procedura składowana programu SQL Server używa tokenu automatycznej rowversion współbieżności:

CREATE PROCEDURE [dbo].[Documents_Update]
    @Id [int],
    @RowVersion_Original [rowversion],
    @Title [nvarchar](max),
    @RowVersion [rowversion] OUT
AS
BEGIN
    DECLARE @TempTable table ([RowVersion] varbinary(8));
    UPDATE [Documents] SET
        [Title] = @Title
    OUTPUT INSERTED.[RowVersion] INTO @TempTable
    WHERE [Id] = @Id AND [RowVersion] = @RowVersion_Original
    SELECT @@ROWCOUNT;
    SELECT @RowVersion = [RowVersion] FROM @TempTable;
END

Jest to skonfigurowane w programie EF Core przy użyciu:

.UpdateUsingStoredProcedure(
    storedProcedureBuilder =>
    {
        storedProcedureBuilder.HasOriginalValueParameter(document => document.Id);
        storedProcedureBuilder.HasOriginalValueParameter(document => document.RowVersion);
        storedProcedureBuilder.HasParameter(document => document.Title);
        storedProcedureBuilder.HasParameter(document => document.RowVersion, parameterBuilder => parameterBuilder.IsOutput());
        storedProcedureBuilder.HasRowsAffectedResultColumn();
    });

Zwróć uwagę, że:

  • Używana jest oryginalna wartość tokenu RowVersion współbieżności.
  • Procedura składowana używa klauzuli WHERE , aby upewnić się, że wiersz jest aktualizowany tylko wtedy, gdy oryginalna wartość jest zgodna RowVersion .
  • Nowa wygenerowana wartość elementu RowVersion jest wstawiana do tabeli tymczasowej.
  • Zwracana jest liczba wierszy, których dotyczy problem (@@ROWCOUNT) i wygenerowana RowVersion wartość.

Mapowanie hierarchii dziedziczenia na procedury składowane

Program EF Core wymaga, aby procedury składowane postępowały zgodnie z układem tabeli dla typów w hierarchii. To oznacza, że:

  • Hierarchia mapowana przy użyciu funkcji TPH musi mieć pojedynczą procedurę składowaną wstawiania, aktualizowania i/lub usuwania przeznaczonej dla pojedynczej zamapowanej tabeli. Procedury składowane wstawiania i aktualizowania muszą mieć parametr wartości dyskryminującej.
  • Hierarchia mapowana przy użyciu TPT musi mieć procedurę składowaną wstawiania, aktualizowania i/lub usuwania dla każdego typu, w tym typów abstrakcyjnych. Program EF Core wykona wiele wywołań w razie potrzeby, aby zaktualizować, wstawić i usunąć wiersze we wszystkich tabelach.
  • Hierarchia mapowana przy użyciu procedury TPC musi mieć procedurę składowaną wstawiania, aktualizowania i/lub usuwania dla każdego typu konkretnego, ale nie abstrakcyjnego.

Uwaga

Jeśli używasz jednej procedury składowanej dla konkretnego typu niezależnie od strategii mapowania jest czymś, co cię interesuje, zagłosuj na pomoc techniczną przy użyciu pojedynczego typu sproc na konkretny typ niezależnie od strategii mapowania dziedziczenia.

Mapowanie typów należących do procedur składowanych

Konfiguracja procedur składowanych dla typów należących do użytkownika odbywa się w zagnieżdżonym konstruktorze typów. Na przykład:

modelBuilder.Entity<Person>(
    entityTypeBuilder =>
    {
        entityTypeBuilder.OwnsOne(
            author => author.Contact,
            ownedNavigationBuilder =>
            {
                ownedNavigationBuilder.ToTable("Contacts");
                ownedNavigationBuilder
                    .InsertUsingStoredProcedure(
                        storedProcedureBuilder =>
                        {
                            storedProcedureBuilder.HasParameter("PersonId");
                            storedProcedureBuilder.HasParameter(contactDetails => contactDetails.Phone);
                        })
                    .UpdateUsingStoredProcedure(
                        storedProcedureBuilder =>
                        {
                            storedProcedureBuilder.HasOriginalValueParameter("PersonId");
                            storedProcedureBuilder.HasParameter(contactDetails => contactDetails.Phone);
                            storedProcedureBuilder.HasRowsAffectedResultColumn();
                        })
                    .DeleteUsingStoredProcedure(
                        storedProcedureBuilder =>
                        {
                            storedProcedureBuilder.HasOriginalValueParameter("PersonId");
                            storedProcedureBuilder.HasRowsAffectedResultColumn();
            });
    });

Uwaga

Obecnie procedury składowane dotyczące wstawiania, aktualizowania i usuwania muszą być mapowane tylko na osobne tabele. Oznacza to, że typ własności nie może być reprezentowany przez kolumny w tabeli właściciela. Zagłosuj na dodanie obsługi podziału "tabeli" do mapowania sproc CUD, jeśli jest to ograniczenie, które chcesz zobaczyć usunięte.

Mapowanie jednostek dołączania wiele do wielu do procedur składowanych

Konfigurację procedur składowanych wiele do wielu jednostek sprzężenia można wykonać w ramach konfiguracji wiele-do-wielu. Przykład:

modelBuilder.Entity<Book>(
    entityTypeBuilder =>
    {
        entityTypeBuilder
            .HasMany(document => document.Authors)
            .WithMany(author => author.PublishedWorks)
            .UsingEntity<Dictionary<string, object>>(
                "BookPerson",
                builder => builder.HasOne<Person>().WithMany().OnDelete(DeleteBehavior.Cascade),
                builder => builder.HasOne<Book>().WithMany().OnDelete(DeleteBehavior.ClientCascade),
                joinTypeBuilder =>
                {
                    joinTypeBuilder
                        .InsertUsingStoredProcedure(
                            storedProcedureBuilder =>
                            {
                                storedProcedureBuilder.HasParameter("AuthorsId");
                                storedProcedureBuilder.HasParameter("PublishedWorksId");
                            })
                        .DeleteUsingStoredProcedure(
                            storedProcedureBuilder =>
                            {
                                storedProcedureBuilder.HasOriginalValueParameter("AuthorsId");
                                storedProcedureBuilder.HasOriginalValueParameter("PublishedWorksId");
                                storedProcedureBuilder.HasRowsAffectedResultColumn();
                            });
                });
    });

Nowe i ulepszone przechwytniki i zdarzenia

Przechwytniki ef Core umożliwiają przechwytywanie, modyfikowanie i/lub pomijanie operacji platformy EF Core. Program EF Core obejmuje również tradycyjne zdarzenia i rejestrowanie platformy .NET.

Program EF7 oferuje następujące ulepszenia przechwytywania:

Ponadto program EF7 obejmuje nowe tradycyjne zdarzenia platformy .NET dla:

W poniższych sekcjach przedstawiono kilka przykładów użycia tych nowych funkcji przechwytywania.

Proste akcje tworzenia jednostki

Napiwek

Pokazany tutaj kod pochodzi z pliku SimpleMaterializationSample.cs.

Nowa IMaterializationInterceptor obsługuje przechwytywanie przed utworzeniem i po utworzeniu wystąpienia jednostki oraz przed i po zainicjowaniu właściwości tego wystąpienia. Przechwytujący może zmienić lub zamienić wystąpienie jednostki w każdym punkcie. Pozwala:

  • Ustawianie niezamapowanych właściwości lub wywoływanie metod wymaganych do walidacji, obliczonych wartości lub flag.
  • Tworzenie wystąpień przy użyciu fabryki.
  • Tworzenie innego wystąpienia jednostki niż program EF zwykle tworzy, na przykład wystąpienie z pamięci podręcznej lub typu serwera proxy.
  • Wstrzykiwanie usług do wystąpienia jednostki.

Załóżmy na przykład, że chcemy śledzić czas pobierania jednostki z bazy danych, być może dlatego może być wyświetlany użytkownikowi edytując dane. Aby to osiągnąć, najpierw zdefiniujemy interfejs:

public interface IHasRetrieved
{
    DateTime Retrieved { get; set; }
}

Używanie interfejsu jest powszechne w przypadku przechwytywania, ponieważ umożliwia to tej samej przechwytywaniu pracę z wieloma różnymi typami jednostek. Przykład:

public class Customer : IHasRetrieved
{
    public int Id { get; set; }
    public string Name { get; set; } = null!;
    public string? PhoneNumber { get; set; }

    [NotMapped]
    public DateTime Retrieved { get; set; }
}

Zwróć uwagę, że [NotMapped] atrybut służy do wskazywania, że ta właściwość jest używana tylko podczas pracy z jednostką i nie powinna być utrwalana w bazie danych.

Przechwytator musi następnie zaimplementować odpowiednią metodę z IMaterializationInterceptor i ustawić pobrany czas:

public class SetRetrievedInterceptor : IMaterializationInterceptor
{
    public object InitializedInstance(MaterializationInterceptionData materializationData, object instance)
    {
        if (instance is IHasRetrieved hasRetrieved)
        {
            hasRetrieved.Retrieved = DateTime.UtcNow;
        }

        return instance;
    }
}

Wystąpienie tego przechwytywania jest rejestrowane podczas konfigurowania elementu DbContext:

public class CustomerContext : DbContext
{
    private static readonly SetRetrievedInterceptor _setRetrievedInterceptor = new();

    public DbSet<Customer> Customers
        => Set<Customer>();

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        => optionsBuilder
            .AddInterceptors(_setRetrievedInterceptor)
            .UseSqlite("Data Source = customers.db");
}

Napiwek

Ten przechwytywanie jest bezstanowe, co jest wspólne, więc pojedyncze wystąpienie jest tworzone i współużytkowane między wszystkimi DbContext wystąpieniami.

Teraz za każdym razem, gdy zostanie Customer wykonane zapytanie z bazy danych, Retrieved właściwość zostanie ustawiona automatycznie. Przykład:

await using (var context = new CustomerContext())
{
    var customer = await context.Customers.SingleAsync(e => e.Name == "Alice");
    Console.WriteLine($"Customer '{customer.Name}' was retrieved at '{customer.Retrieved.ToLocalTime()}'");
}

Generuje dane wyjściowe:

Customer 'Alice' was retrieved at '9/22/2022 5:25:54 PM'

Wstrzykiwanie usług do jednostek

Napiwek

Pokazany tutaj kod pochodzi z pliku InjectLoggerSample.cs.

Program EF Core ma już wbudowaną obsługę wstrzykiwania niektórych specjalnych usług do wystąpień kontekstowych; na przykład zobacz Ładowanie z opóźnieniem bez serwerów proxy, które działa przez wstrzyknięcie ILazyLoader usługi.

Element IMaterializationInterceptor może służyć do uogólninia tej usługi w dowolnej usłudze. W poniższym przykładzie pokazano, jak wstrzyknąć element ILogger do jednostek, tak aby mogły wykonywać własne rejestrowanie.

Uwaga

Wstrzykiwanie usług do jednostek łączy te typy jednostek z wstrzykniętymi usługami, które niektórzy użytkownicy uważają za antywzór.

Tak jak poprzednio, interfejs służy do definiowania, co można zrobić.

public interface IHasLogger
{
    ILogger? Logger { get; set; }
}

Typy jednostek, które będą rejestrować, muszą implementować ten interfejs. Przykład:

public class Customer : IHasLogger
{
    private string? _phoneNumber;

    public int Id { get; set; }
    public string Name { get; set; } = null!;

    public string? PhoneNumber
    {
        get => _phoneNumber;
        set
        {
            Logger?.LogInformation(1, $"Updating phone number for '{Name}' from '{_phoneNumber}' to '{value}'.");

            _phoneNumber = value;
        }
    }

    [NotMapped]
    public ILogger? Logger { get; set; }
}

Tym razem przechwytywanie musi zaimplementować IMaterializationInterceptor.InitializedInstanceelement , który jest wywoływany po utworzeniu każdego wystąpienia jednostki, a jego wartości właściwości zostały zainicjowane. Przechwytujący uzyskuje element ILogger z kontekstu i inicjuje IHasLogger.Logger go:

public class LoggerInjectionInterceptor : IMaterializationInterceptor
{
    private ILogger? _logger;

    public object InitializedInstance(MaterializationInterceptionData materializationData, object instance)
    {
        if (instance is IHasLogger hasLogger)
        {
            _logger ??= materializationData.Context.GetService<ILoggerFactory>().CreateLogger("CustomersLogger");
            hasLogger.Logger = _logger;
        }

        return instance;
    }
}

Tym razem jest używane nowe wystąpienie przechwytywania dla każdego DbContext wystąpienia, ponieważ ILogger uzyskane może ulec zmianie na DbContext wystąpienie, a ILogger element jest buforowany na przechwytnika:

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    => optionsBuilder.AddInterceptors(new LoggerInjectionInterceptor());

Teraz za każdym razem, gdy Customer.PhoneNumber ta zmiana zostanie zmieniona, ta zmiana zostanie zarejestrowana w dzienniku aplikacji. Przykład:

info: CustomersLogger[1]
      Updating phone number for 'Alice' from '+1 515 555 0123' to '+1 515 555 0125'.

Przechwytywanie drzewa wyrażeń LINQ

Napiwek

Pokazany tutaj kod pochodzi z pliku QueryInterceptionSample.cs.

Program EF Core korzysta z zapytań LINQ platformy .NET. Zwykle wiąże się to z użyciem kompilatora języka C#, VB lub F# w celu utworzenia drzewa wyrażeń, które następnie jest tłumaczone przez program EF Core na odpowiedni język SQL. Rozważmy na przykład metodę zwracającą stronę klientów:

Task<List<Customer>> GetPageOfCustomers(string sortProperty, int page)
{
    using var context = new CustomerContext();

    return context.Customers
        .OrderBy(e => EF.Property<object>(e, sortProperty))
        .Skip(page * 20).Take(20).ToListAsync();
}

Napiwek

To zapytanie używa EF.Property metody , aby określić właściwość do sortowania. Dzięki temu aplikacja może dynamicznie przekazywać nazwę właściwości, umożliwiając sortowanie według dowolnej właściwości typu jednostki. Należy pamiętać, że sortowanie według kolumn nieindeksowanych może być powolne.

Będzie to działać prawidłowo, o ile właściwość używana do sortowania zawsze zwraca stabilną kolejność. Ale to nie zawsze może być tak. Na przykład powyższe zapytanie LINQ generuje następujące polecenie w sqlite podczas zamawiania według :Customer.City

SELECT "c"."Id", "c"."City", "c"."Name", "c"."PhoneNumber"
FROM "Customers" AS "c"
ORDER BY "c"."City"
LIMIT @__p_1 OFFSET @__p_0

Jeśli istnieje wielu klientów z tym samym Cityelementem , kolejność tego zapytania nie jest stabilna. Może to prowadzić do braku lub zduplikowania wyników jako stron użytkownika za pośrednictwem danych.

Typowym sposobem rozwiązania tego problemu jest wykonanie pomocniczego sortowania według klucza podstawowego. Jednak zamiast ręcznie dodawać je do każdego zapytania, program EF7 umożliwia przechwycenie drzewa wyrażeń zapytania, w którym można dynamicznie dodać kolejność pomocniczą. Aby to ułatwić, ponownie użyjemy interfejsu, tym razem dla każdej jednostki, która ma klucz podstawowy liczby całkowitej:

public interface IHasIntKey
{
    int Id { get; }
}

Ten interfejs jest implementowany przez interesujące typy jednostek:

public class Customer : IHasIntKey
{
    public int Id { get; set; }
    public string Name { get; set; } = null!;
    public string? City { get; set; }
    public string? PhoneNumber { get; set; }
}

Następnie potrzebujemy przechwytnika, który implementuje IQueryExpressionInterceptor

public class KeyOrderingExpressionInterceptor : IQueryExpressionInterceptor
{
    public Expression QueryCompilationStarting(Expression queryExpression, QueryExpressionEventData eventData)
        => new KeyOrderingExpressionVisitor().Visit(queryExpression);

    private class KeyOrderingExpressionVisitor : ExpressionVisitor
    {
        private static readonly MethodInfo ThenByMethod
            = typeof(Queryable).GetMethods()
                .Single(m => m.Name == nameof(Queryable.ThenBy) && m.GetParameters().Length == 2);

        protected override Expression VisitMethodCall(MethodCallExpression? methodCallExpression)
        {
            var methodInfo = methodCallExpression!.Method;
            if (methodInfo.DeclaringType == typeof(Queryable)
                && methodInfo.Name == nameof(Queryable.OrderBy)
                && methodInfo.GetParameters().Length == 2)
            {
                var sourceType = methodCallExpression.Type.GetGenericArguments()[0];
                if (typeof(IHasIntKey).IsAssignableFrom(sourceType))
                {
                    var lambdaExpression = (LambdaExpression)((UnaryExpression)methodCallExpression.Arguments[1]).Operand;
                    var entityParameterExpression = lambdaExpression.Parameters[0];

                    return Expression.Call(
                        ThenByMethod.MakeGenericMethod(
                            sourceType,
                            typeof(int)),
                        base.VisitMethodCall(methodCallExpression),
                        Expression.Lambda(
                            typeof(Func<,>).MakeGenericType(entityParameterExpression.Type, typeof(int)),
                            Expression.Property(entityParameterExpression, nameof(IHasIntKey.Id)),
                            entityParameterExpression));
                }
            }

            return base.VisitMethodCall(methodCallExpression);
        }
    }
}

To prawdopodobnie wygląda dość skomplikowane - i to jest! Praca z drzewami wyrażeń zwykle nie jest łatwa. Przyjrzyjmy się temu, co się dzieje:

  • Zasadniczo przechwytujący hermetyzuje ExpressionVisitorelement . Odwiedzający zastąpi metodę VisitMethodCall, która będzie wywoływana za każdym razem, gdy w drzewie wyrażeń zapytania występuje wywołanie metody.

  • Odwiedzający sprawdza, czy jest to wywołanie OrderBy metody, która nas interesuje.

  • Jeśli tak jest, odwiedzający dalej sprawdza, czy wywołanie metody ogólnej jest dla typu, który implementuje nasz IHasIntKey interfejs.

  • W tym momencie wiemy, że wywołanie metody ma postać OrderBy(e => ...). Wyodrębniamy wyrażenie lambda z tego wywołania i pobieramy parametr używany w tym wyrażeniu e— czyli .

  • Teraz tworzymy nowe MethodCallExpression przy użyciu metody konstruktora Expression.Call . W takim przypadku wywoływana metoda to ThenBy(e => e.Id). Tworzymy to przy użyciu parametru wyodrębnionego powyżej i dostępu właściwości do Id właściwości interfejsu IHasIntKey .

  • Dane wejściowe do tego wywołania są oryginalne OrderBy(e => ...), a więc wynik końcowy jest wyrażeniem dla OrderBy(e => ...).ThenBy(e => e.Id)elementu .

  • To zmodyfikowane wyrażenie jest zwracane przez odwiedzających, co oznacza, że zapytanie LINQ zostało odpowiednio zmodyfikowane w celu dołączenia wywołania ThenBy .

  • Program EF Core kontynuuje i kompiluje to wyrażenie zapytania do odpowiedniego języka SQL dla używanej bazy danych.

Ten przechwytywanie jest rejestrowane w taki sam sposób, jak w przypadku pierwszego przykładu. GetPageOfCustomers Wykonanie powoduje teraz wygenerowanie następującego kodu SQL:

SELECT "c"."Id", "c"."City", "c"."Name", "c"."PhoneNumber"
FROM "Customers" AS "c"
ORDER BY "c"."City", "c"."Id"
LIMIT @__p_1 OFFSET @__p_0

Teraz zawsze będzie to generować stabilne zamówienie, nawet jeśli istnieje wielu klientów z tym samym City.

Uff! Jest to dużo kodu, aby wprowadzić prostą zmianę w zapytaniu. Co gorsza, może nawet nie działać dla wszystkich zapytań. Notorycznie trudno jest napisać odwiedzającym wyrażenie, które rozpoznaje wszystkie kształty zapytania, których powinien, i żaden z tych, których nie powinien. Na przykład prawdopodobnie nie zadziała to, jeśli kolejność jest wykonywana w podzapytaniu.

To prowadzi nas do krytycznego punktu na temat przechwytujących - zawsze pytaj siebie, czy istnieje łatwiejszy sposób robienia tego, co chcesz. Przechwytniki są potężne, ale łatwo jest coś złego. Są one, jak mówi mówi, łatwy sposób, aby strzelić się w stopę.

Załóżmy na przykład, że zamiast tego zmieniliśmy naszą GetPageOfCustomers metodę w następujący sposób:

Task<List<Customer>> GetPageOfCustomers2(string sortProperty, int page)
{
    using var context = new CustomerContext();

    return context.Customers
        .OrderBy(e => EF.Property<object>(e, sortProperty))
        .ThenBy(e => e.Id)
        .Skip(page * 20).Take(20).ToListAsync();
}

W takim przypadku element ThenBy jest po prostu dodawany do zapytania. Tak, może być konieczne wykonanie go oddzielnie dla każdego zapytania, ale jest proste, łatwe do zrozumienia i zawsze będzie działać.

Optymistyczne przechwycenie współbieżności

Napiwek

Pokazany tutaj kod pochodzi z pliku OptimisticConcurrencyInterceptionSample.cs.

Program EF Core obsługuje optymistyczny wzorzec współbieżności, sprawdzając, czy liczba wierszy, których dotyczy aktualizacja lub usunięcie, jest taka sama jak liczba wierszy, których oczekuje się, że będzie to miało wpływ. Jest to często powiązane z tokenem współbieżności; oznacza to, że wartość kolumny, która będzie zgodna tylko z oczekiwaną wartością, jeśli wiersz nie został zaktualizowany od czasu odczytania oczekiwanej wartości.

Ef sygnalizuje naruszenie optymistycznej współbieżności, zgłaszając element DbUpdateConcurrencyException. W programie EF7 istnieją ISaveChangesInterceptor nowe metody ThrowingConcurrencyException , ThrowingConcurrencyExceptionAsync które są wywoływane przed zgłoszeniem DbUpdateConcurrencyException . Te punkty przechwytywania umożliwiają pomijanie wyjątku, prawdopodobnie w połączeniu ze zmianami asynchronicznych bazy danych w celu rozwiązania naruszenia.

Jeśli na przykład dwa żądania spróbują usunąć tę samą jednostkę w niemal tym samym czasie, drugie usunięcie może zakończyć się niepowodzeniem, ponieważ wiersz w bazie danych już nie istnieje. Może to być wynik końcowy, ponieważ mimo to jednostka została usunięta. Poniższy przechwytujący pokazuje, jak można to zrobić:

public class SuppressDeleteConcurrencyInterceptor : ISaveChangesInterceptor
{
    public InterceptionResult ThrowingConcurrencyException(
        ConcurrencyExceptionEventData eventData,
        InterceptionResult result)
    {
        if (eventData.Entries.All(e => e.State == EntityState.Deleted))
        {
            Console.WriteLine("Suppressing Concurrency violation for command:");
            Console.WriteLine(((RelationalConcurrencyExceptionEventData)eventData).Command.CommandText);

            return InterceptionResult.Suppress();
        }

        return result;
    }

    public ValueTask<InterceptionResult> ThrowingConcurrencyExceptionAsync(
        ConcurrencyExceptionEventData eventData,
        InterceptionResult result,
        CancellationToken cancellationToken = default)
        => new(ThrowingConcurrencyException(eventData, result));
}

Istnieje kilka rzeczy, które warto zauważyć o tym przechwytywaniu:

  • Implementowane są zarówno metody przechwytywania synchronicznego, jak i asynchronicznego. Jest to ważne, jeśli aplikacja może wywołać metodę SaveChanges lub SaveChangesAsync. Jeśli jednak cały kod aplikacji jest asynchroniczny, należy zaimplementować tylko ThrowingConcurrencyExceptionAsync te elementy. Podobnie, jeśli aplikacja nigdy nie używa metod asynchronicznych bazy danych, należy zaimplementować tylko ThrowingConcurrencyException te metody. Zwykle dotyczy to wszystkich przechwytujących metod synchronizacji i asynchronicznych. (Warto zaimplementować metodę, która nie jest używana do zgłaszania przez aplikację, na wypadek, gdy część kodu synchronizacji/asynchronicznego wkradnie się).
  • Przechwytujący ma dostęp do EntityEntry obiektów dla zapisywanych jednostek. W takim przypadku służy to do sprawdzania, czy naruszenie współbieżności ma miejsce dla operacji usuwania.
  • Jeśli aplikacja korzysta z dostawcy relacyjnej bazy danych, ConcurrencyExceptionEventData obiekt można rzutować do RelationalConcurrencyExceptionEventData obiektu. Zapewnia to dodatkowe, specyficzne dla relacyjne informacje o wykonywanej operacji bazy danych. W takim przypadku tekst polecenia relacyjnego jest drukowany w konsoli programu .
  • Zwracanie InterceptionResult.Suppress() informuje platformę EF Core, aby pominąć akcję, która miała na celu podjęcie — w tym przypadku zgłasza błąd DbUpdateConcurrencyException. Ta możliwość zmiany zachowania platformy EF Core, a nie tylko obserwowania tego, co robi program EF Core, jest jedną z najbardziej zaawansowanych funkcji przechwytywania.

Opóźnij inicjowanie parametry połączenia

Napiwek

Pokazany tutaj kod pochodzi z pliku Lazy Połączenie ionStringSample.cs.

ciągi Połączenie ion są często statycznymi elementami zawartości odczytywanym z pliku konfiguracji. Można je łatwo przekazać do UseSqlServer lub podobnego podczas konfigurowania obiektu DbContext. Jednak czasami parametry połączenia może ulec zmianie dla każdego wystąpienia kontekstu. Na przykład każda dzierżawa w systemie z wieloma dzierżawami może mieć inne parametry połączenia.

Program EF7 ułatwia obsługę połączeń dynamicznych i parametry połączenia dzięki ulepszeniom systemu IDbConnectionInterceptor. Zaczyna się to od możliwości skonfigurowania DbContext bez żadnych parametry połączenia. Przykład:

services.AddDbContext<CustomerContext>(
    b => b.UseSqlServer());

IDbConnectionInterceptor Jedną z metod można następnie zaimplementować w celu skonfigurowania połączenia przed jego użyciem. ConnectionOpeningAsyncjest dobrym wyborem, ponieważ może wykonać operację asynchroniową w celu uzyskania parametry połączenia, znalezienia tokenu dostępu itd. Załóżmy na przykład, że usługa jest ograniczona do bieżącego żądania, które rozumie bieżącą dzierżawę:

services.AddScoped<ITenantConnectionStringFactory, TestTenantConnectionStringFactory>();

Ostrzeżenie

Wykonywanie asynchronicznego wyszukiwania dla parametry połączenia, tokenu dostępu lub podobnego za każdym razem, gdy jest to konieczne, może być bardzo powolne. Rozważ buforowanie tych elementów i okresowe odświeżanie buforowanego ciągu lub tokenu. Na przykład tokeny dostępu mogą być często używane przez dłuższy czas przed koniecznością odświeżenia.

Można to wstrzyknąć do każdego DbContext wystąpienia przy użyciu iniekcji konstruktora:

public class CustomerContext : DbContext
{
    private readonly ITenantConnectionStringFactory _connectionStringFactory;

    public CustomerContext(
        DbContextOptions<CustomerContext> options,
        ITenantConnectionStringFactory connectionStringFactory)
        : base(options)
    {
        _connectionStringFactory = connectionStringFactory;
    }

    // ...
}

Ta usługa jest następnie używana podczas konstruowania implementacji przechwytywania dla kontekstu:

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    => optionsBuilder.AddInterceptors(
        new ConnectionStringInitializationInterceptor(_connectionStringFactory));

Na koniec przechwytywanie używa tej usługi do uzyskiwania parametry połączenia asynchronicznie i ustawia go po raz pierwszy, gdy połączenie jest używane:

public class ConnectionStringInitializationInterceptor : DbConnectionInterceptor
{
    private readonly IClientConnectionStringFactory _connectionStringFactory;

    public ConnectionStringInitializationInterceptor(IClientConnectionStringFactory connectionStringFactory)
    {
        _connectionStringFactory = connectionStringFactory;
    }

    public override InterceptionResult ConnectionOpening(
        DbConnection connection,
        ConnectionEventData eventData,
        InterceptionResult result)
        => throw new NotSupportedException("Synchronous connections not supported.");

    public override async ValueTask<InterceptionResult> ConnectionOpeningAsync(
        DbConnection connection, ConnectionEventData eventData, InterceptionResult result,
        CancellationToken cancellationToken = new())
    {
        if (string.IsNullOrEmpty(connection.ConnectionString))
        {
            connection.ConnectionString = (await _connectionStringFactory.GetConnectionStringAsync(cancellationToken));
        }

        return result;
    }
}

Uwaga

Parametry połączenia jest uzyskiwany tylko przy pierwszym użyciu połączenia. Następnie parametry połączenia przechowywane w DbConnection obiekcie będą używane bez wyszukiwania nowego parametry połączenia.

Napiwek

Ten przechwytywanie zastępuje metodę inną niż asynchronikowąConnectionOpening, ponieważ usługa w celu pobrania parametry połączenia musi być wywoływana ze ścieżki kodu asynchronicznego.

Rejestrowanie statystyk zapytań programu SQL Server

Napiwek

Pokazany tutaj kod pochodzi z pliku QueryStatisticsLoggerSample.cs.

Na koniec utwórzmy dwa przechwytniki, które współpracują ze sobą w celu wysyłania statystyk zapytań programu SQL Server do dziennika aplikacji. Aby wygenerować statystyki, musimy IDbCommandInterceptor wykonać dwie czynności.

Najpierw przechwytuje polecenia prefiksu za pomocą SET STATISTICS IO ONpolecenia , które informują program SQL Server o wysyłaniu statystyk do klienta po zużytym zestawie wyników:

public override ValueTask<InterceptionResult<DbDataReader>> ReaderExecutingAsync(
    DbCommand command,
    CommandEventData eventData,
    InterceptionResult<DbDataReader> result,
    CancellationToken cancellationToken = default)
{
    command.CommandText = "SET STATISTICS IO ON;" + Environment.NewLine + command.CommandText;

    return new(result);
}

Po drugie, przechwytator zaimplementuje nową DataReaderClosingAsync metodę, która jest wywoływana po zakończeniu DbDataReader korzystania z wyników, ale zanim zostanie zamknięta. Gdy program SQL Server wysyła statystyki, umieszcza je w drugim wyniku na czytniku, więc w tym momencie przechwytujący odczytuje ten wynik przez wywołanie NextResultAsync , które wypełnia statystyki na połączeniu.

public override async ValueTask<InterceptionResult> DataReaderClosingAsync(
    DbCommand command,
    DataReaderClosingEventData eventData,
    InterceptionResult result)
{
    await eventData.DataReader.NextResultAsync();

    return result;
}

Drugi przechwytujący jest potrzebny do uzyskania statystyk z połączenia i zapisania ich w rejestratorze aplikacji. W tym celu użyjemy IDbConnectionInterceptormetody , implementowania nowej ConnectionCreated metody. ConnectionCreated jest wywoływana natychmiast po utworzeniu połączenia przez program EF Core, a więc może służyć do wykonywania dodatkowej konfiguracji tego połączenia. W takim przypadku przechwytujący uzyskuje element ILogger , a następnie przechwytuje zdarzenie w SqlConnection.InfoMessage celu zarejestrowania komunikatów.

public override DbConnection ConnectionCreated(ConnectionCreatedEventData eventData, DbConnection result)
{
    var logger = eventData.Context!.GetService<ILoggerFactory>().CreateLogger("InfoMessageLogger");
    ((SqlConnection)eventData.Connection).InfoMessage += (_, args) =>
    {
        logger.LogInformation(1, args.Message);
    };
    return result;
}

Ważne

Metody ConnectionCreating i ConnectionCreated są wywoływane tylko wtedy, gdy program EF Core tworzy DbConnectionobiekt . Nie będą one wywoływane, jeśli aplikacja utworzy obiekt DbConnection i przekaże ją do platformy EF Core.

Uruchomienie kodu korzystającego z tych przechwytywania pokazuje statystyki zapytań programu SQL Server w dzienniku:

info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (4ms) [Parameters=[@p0='?' (Size = 4000), @p1='?' (Size = 4000), @p2='?' (Size = 4000), @p3='?' (Size = 4000)], CommandType='Text', CommandTimeout='30']
      SET STATISTICS IO ON;
      SET IMPLICIT_TRANSACTIONS OFF;
      SET NOCOUNT ON;
      MERGE [Customers] USING (
      VALUES (@p0, @p1, 0),
      (@p2, @p3, 1)) AS i ([Name], [PhoneNumber], _Position) ON 1=0
      WHEN NOT MATCHED THEN
      INSERT ([Name], [PhoneNumber])
      VALUES (i.[Name], i.[PhoneNumber])
      OUTPUT INSERTED.[Id], i._Position;
info: InfoMessageLogger[1]
      Table 'Customers'. Scan count 0, logical reads 5, physical reads 0, page server reads 0, read-ahead reads 0, page server read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob page server reads 0, lob read-ahead reads 0, lob page server read-ahead reads 0.
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (2ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      SET STATISTICS IO ON;
      SELECT TOP(2) [c].[Id], [c].[Name], [c].[PhoneNumber]
      FROM [Customers] AS [c]
      WHERE [c].[Name] = N'Alice'
info: InfoMessageLogger[1]
      Table 'Customers'. Scan count 1, logical reads 2, physical reads 0, page server reads 0, read-ahead reads 0, page server read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob page server reads 0, lob read-ahead reads 0, lob page server read-ahead reads 0.

Ulepszenia zapytań

PROGRAM EF7 zawiera wiele ulepszeń tłumaczenia zapytań LINQ.

GroupBy jako operator końcowy

Napiwek

Pokazany tutaj kod pochodzi z pliku GroupByFinalOperatorSample.cs.

Program EF7 obsługuje używanie GroupBy jako operatora końcowego w zapytaniu. Na przykład to zapytanie LINQ:

var query = context.Books.GroupBy(s => s.Price);

Przekłada się na następujący kod SQL podczas korzystania z programu SQL Server:

SELECT [b].[Price], [b].[Id], [b].[AuthorId]
FROM [Books] AS [b]
ORDER BY [b].[Price]

Uwaga

Ten typ GroupBy nie przekłada się bezpośrednio na język SQL, dlatego program EF Core wykonuje grupowanie w zwróconych wynikach. Nie powoduje to jednak transferu żadnych dodatkowych danych z serwera.

GroupJoin jako operator końcowy

Napiwek

Pokazany tutaj kod pochodzi z pliku GroupJoinFinalOperatorSample.cs.

Program EF7 obsługuje używanie GroupJoin jako operatora końcowego w zapytaniu. Na przykład to zapytanie LINQ:

var query = context.Customers.GroupJoin(
    context.Orders, c => c.Id, o => o.CustomerId, (c, os) => new { Customer = c, Orders = os });

Przekłada się na następujący kod SQL podczas korzystania z programu SQL Server:

SELECT [c].[Id], [c].[Name], [t].[Id], [t].[Amount], [t].[CustomerId]
FROM [Customers] AS [c]
OUTER APPLY (
    SELECT [o].[Id], [o].[Amount], [o].[CustomerId]
    FROM [Orders] AS [o]
    WHERE [c].[Id] = [o].[CustomerId]
) AS [t]
ORDER BY [c].[Id]

Typ jednostki Grupuj wg

Napiwek

Pokazany tutaj kod pochodzi z pliku GroupByEntityTypeSample.cs.

Program EF7 obsługuje grupowanie według typu jednostki. Na przykład to zapytanie LINQ:

var query = context.Books
    .GroupBy(s => s.Author)
    .Select(s => new { Author = s.Key, MaxPrice = s.Max(p => p.Price) });

Tłumaczy się na następujący kod SQL podczas korzystania z biblioteki SQLite:

SELECT [a].[Id], [a].[Name], MAX([b].[Price]) AS [MaxPrice]
FROM [Books] AS [b]
INNER JOIN [Author] AS [a] ON [b].[AuthorId] = [a].[Id]
GROUP BY [a].[Id], [a].[Name]

Należy pamiętać, że grupowanie według unikatowej właściwości, takiej jak klucz podstawowy, zawsze będzie bardziej wydajne niż grupowanie według typu jednostki. Jednak grupowanie według typów jednostek może być używane zarówno dla typów jednostek kluczy, jak i bez klucza.

Ponadto grupowanie według typu jednostki z kluczem podstawowym zawsze spowoduje utworzenie jednej grupy na wystąpienie jednostki, ponieważ każda jednostka musi mieć unikatową wartość klucza. Czasami warto przełączyć źródło zapytania, aby nie było wymagane grupowanie. Na przykład następujące zapytanie zwraca te same wyniki co poprzednie zapytanie:

var query = context.Authors
    .Select(a => new { Author = a, MaxPrice = a.Books.Max(b => b.Price) });

To zapytanie przekłada się na następujący kod SQL podczas korzystania z biblioteki SQLite:

SELECT [a].[Id], [a].[Name], (
    SELECT MAX([b].[Price])
    FROM [Books] AS [b]
    WHERE [a].[Id] = [b].[AuthorId]) AS [MaxPrice]
FROM [Authors] AS [a]

Podzapytania nie odwołują się do niezgrupowanych kolumn z zapytania zewnętrznego

Napiwek

Pokazany tutaj kod pochodzi z pliku UngroupedColumnsQuerySample.cs.

W programie EF Core 6.0 klauzula GROUP BY odwołuje się do kolumn w zapytaniu zewnętrznym, która kończy się niepowodzeniem z niektórymi bazami danych i jest nieefektywna w innych. Rozważmy na przykład następujące zapytanie:

var query = from s in (from i in context.Invoices
                       group i by i.History.Month
                       into g
                       select new { Month = g.Key, Total = g.Sum(p => p.Amount), })
            select new
            {
                s.Month, s.Total, Payment = context.Payments.Where(p => p.History.Month == s.Month).Sum(p => p.Amount)
            };

W programie EF Core 6.0 w programie SQL Server zostało to przetłumaczone na:

SELECT DATEPART(month, [i].[History]) AS [Month], COALESCE(SUM([i].[Amount]), 0.0) AS [Total], (
    SELECT COALESCE(SUM([p].[Amount]), 0.0)
    FROM [Payments] AS [p]
    WHERE DATEPART(month, [p].[History]) = DATEPART(month, [i].[History])) AS [Payment]
FROM [Invoices] AS [i]
GROUP BY DATEPART(month, [i].[History])

W programie EF7 tłumaczenie brzmi:

SELECT [t].[Key] AS [Month], COALESCE(SUM([t].[Amount]), 0.0) AS [Total], (
    SELECT COALESCE(SUM([p].[Amount]), 0.0)
    FROM [Payments] AS [p]
    WHERE DATEPART(month, [p].[History]) = [t].[Key]) AS [Payment]
FROM (
    SELECT [i].[Amount], DATEPART(month, [i].[History]) AS [Key]
    FROM [Invoices] AS [i]
) AS [t]
GROUP BY [t].[Key]

Kolekcje tylko do odczytu mogą być używane dla Contains

Napiwek

Pokazany tutaj kod pochodzi z pliku ReadOnlySetQuerySample.cs.

Program EF7 obsługuje funkcję , Contains gdy elementy do wyszukania znajdują się w elemecie IReadOnlySet lub IReadOnlyCollectionlub IReadOnlyList. Na przykład to zapytanie LINQ:

IReadOnlySet<int> searchIds = new HashSet<int> { 1, 3, 5 };
var query = context.Customers.Where(p => p.Orders.Any(l => searchIds.Contains(l.Id)));

Przekłada się na następujący kod SQL podczas korzystania z programu SQL Server:

SELECT [c].[Id], [c].[Name]
FROM [Customers] AS [c]
WHERE EXISTS (
    SELECT 1
    FROM [Orders] AS [o]
    WHERE [c].[Id] = [o].[Customer1Id] AND [o].[Id] IN (1, 3, 5))

Tłumaczenia dla funkcji agregujących

Program EF7 wprowadza lepszą rozszerzalność dla dostawców w celu tłumaczenia funkcji agregujących. To i inne prace w tym obszarze doprowadziły do kilku nowych tłumaczeń między dostawcami, w tym:

Uwaga

Funkcje agregujące, które działają na IEnumerable argument, są zwykle tłumaczone tylko w GroupBy zapytaniach. Zagłosuj na obsługę typów przestrzennych w kolumnach JSON, jeśli chcesz usunąć to ograniczenie.

Funkcje agregujące ciągi

Napiwek

Pokazany tutaj kod pochodzi z pliku StringAggregateFunctionsSample.cs.

Zapytania korzystające z funkcji Join i Concat są teraz tłumaczone, gdy jest to konieczne. Przykład:

var query = context.Posts
    .GroupBy(post => post.Author)
    .Select(grouping => new { Author = grouping.Key, Books = string.Join("|", grouping.Select(post => post.Title)) });

To zapytanie przekłada się na następujące kwestie podczas korzystania z programu SQL Server:

SELECT [a].[Id], [a].[Name], COALESCE(STRING_AGG([p].[Title], N'|'), N'') AS [Books]
FROM [Posts] AS [p]
LEFT JOIN [Authors] AS [a] ON [p].[AuthorId] = [a].[Id]
GROUP BY [a].[Id], [a].[Name]

W połączeniu z innymi funkcjami ciągów te tłumaczenia umożliwiają pewne złożone manipulowanie ciągami na serwerze. Przykład:

var query = context.Posts
    .GroupBy(post => post.Author!.Name)
    .Select(
        grouping =>
            new
            {
                PostAuthor = grouping.Key,
                Blogs = string.Concat(
                    grouping
                        .Select(post => post.Blog.Name)
                        .Distinct()
                        .Select(postName => "'" + postName + "' ")),
                ContentSummaries = string.Join(
                    " | ",
                    grouping
                        .Where(post => post.Content.Length >= 10)
                        .Select(post => "'" + post.Content.Substring(0, 10) + "' "))
            });

To zapytanie przekłada się na następujące kwestie podczas korzystania z programu SQL Server:

SELECT [t].[Name], (N'''' + [t0].[Name]) + N''' ', [t0].[Name], [t].[c]
FROM (
    SELECT [a].[Name], COALESCE(STRING_AGG(CASE
        WHEN CAST(LEN([p].[Content]) AS int) >= 10 THEN COALESCE((N'''' + COALESCE(SUBSTRING([p].[Content], 0 + 1, 10), N'')) + N''' ', N'')
    END, N' | '), N'') AS [c]
    FROM [Posts] AS [p]
    LEFT JOIN [Authors] AS [a] ON [p].[AuthorId] = [a].[Id]
    GROUP BY [a].[Name]
) AS [t]
OUTER APPLY (
    SELECT DISTINCT [b].[Name]
    FROM [Posts] AS [p0]
    LEFT JOIN [Authors] AS [a0] ON [p0].[AuthorId] = [a0].[Id]
    INNER JOIN [Blogs] AS [b] ON [p0].[BlogId] = [b].[Id]
    WHERE [t].[Name] = [a0].[Name] OR ([t].[Name] IS NULL AND [a0].[Name] IS NULL)
) AS [t0]
ORDER BY [t].[Name]

Funkcje agregacji przestrzennej

Napiwek

Pokazany tutaj kod pochodzi z pliku SpatialAggregateFunctionsSample.cs.

Obecnie jest możliwe, aby dostawcy baz danych, którzy obsługują aplikację NetTopologySuite , aby przetłumaczyć następujące funkcje agregacji przestrzennej:

Napiwek

Te tłumaczenia zostały zaimplementowane przez zespół dla programu SQL Server i sqLite. W przypadku innych dostawców skontaktuj się z opiekunem dostawcy, aby dodać pomoc techniczną, jeśli został on wdrożony dla tego dostawcy.

Przykład:

var query = context.Caches
    .Where(cache => cache.Location.X < -90)
    .GroupBy(cache => cache.Owner)
    .Select(
        grouping => new { Id = grouping.Key, Combined = GeometryCombiner.Combine(grouping.Select(cache => cache.Location)) });

To zapytanie jest tłumaczone na następujący kod SQL podczas korzystania z programu SQL Server:

SELECT [c].[Owner] AS [Id], geography::CollectionAggregate([c].[Location]) AS [Combined]
FROM [Caches] AS [c]
WHERE [c].[Location].Long < -90.0E0
GROUP BY [c].[Owner]

Funkcje agregacji statystycznej

Napiwek

Pokazany tutaj kod pochodzi z pliku StatisticalAggregateFunctionsSample.cs.

Tłumaczenia programu SQL Server zostały zaimplementowane dla następujących funkcji statystycznych:

Napiwek

Te tłumaczenia zostały zaimplementowane przez zespół dla programu SQL Server. W przypadku innych dostawców skontaktuj się z opiekunem dostawcy, aby dodać pomoc techniczną, jeśli został on wdrożony dla tego dostawcy.

Przykład:

var query = context.Downloads
    .GroupBy(download => download.Uploader.Id)
    .Select(
        grouping => new
        {
            Author = grouping.Key,
            TotalCost = grouping.Sum(d => d.DownloadCount),
            AverageViews = grouping.Average(d => d.DownloadCount),
            VariancePopulation = EF.Functions.VariancePopulation(grouping.Select(d => d.DownloadCount)),
            VarianceSample = EF.Functions.VarianceSample(grouping.Select(d => d.DownloadCount)),
            StandardDeviationPopulation = EF.Functions.StandardDeviationPopulation(grouping.Select(d => d.DownloadCount)),
            StandardDeviationSample = EF.Functions.StandardDeviationSample(grouping.Select(d => d.DownloadCount))
        });

To zapytanie jest tłumaczone na następujący kod SQL podczas korzystania z programu SQL Server:

SELECT [u].[Id] AS [Author], COALESCE(SUM([d].[DownloadCount]), 0) AS [TotalCost], AVG(CAST([d].[DownloadCount] AS float)) AS [AverageViews], VARP([d].[DownloadCount]) AS [VariancePopulation], VAR([d].[DownloadCount]) AS [VarianceSample], STDEVP([d].[DownloadCount]) AS [StandardDeviationPopulation], STDEV([d].[DownloadCount]) AS [StandardDeviationSample]
FROM [Downloads] AS [d]
INNER JOIN [Uploader] AS [u] ON [d].[UploaderId] = [u].[Id]
GROUP BY [u].[Id]

Tłumaczenie string.IndexOf

Napiwek

Pokazany tutaj kod pochodzi z pliku MiscellaneousTranslationsSample.cs.

Program EF7 tłumaczy String.IndexOf teraz w zapytaniach LINQ. Przykład:

var query = context.Posts
    .Select(post => new { post.Title, IndexOfEntity = post.Content.IndexOf("Entity") })
    .Where(post => post.IndexOfEntity > 0);

To zapytanie przekłada się na następujący kod SQL podczas korzystania z programu SQL Server:

SELECT [p].[Title], CAST(CHARINDEX(N'Entity', [p].[Content]) AS int) - 1 AS [IndexOfEntity]
FROM [Posts] AS [p]
WHERE (CAST(CHARINDEX(N'Entity', [p].[Content]) AS int) - 1) > 0

GetType Tłumaczenie typów jednostek

Napiwek

Pokazany tutaj kod pochodzi z pliku MiscellaneousTranslationsSample.cs.

Program EF7 tłumaczy Object.GetType() teraz w zapytaniach LINQ. Przykład:

var query = context.Posts.Where(post => post.GetType() == typeof(Post));

To zapytanie przekłada się na następujący kod SQL podczas korzystania z programu SQL Server z dziedziczeniem TPH:

SELECT [p].[Id], [p].[AuthorId], [p].[BlogId], [p].[Content], [p].[Discriminator], [p].[PublishedOn], [p].[Title], [p].[PromoText]
FROM [Posts] AS [p]
WHERE [p].[Discriminator] = N'Post'

Zwróć uwagę, że to zapytanie zwraca tylko Post wystąpienia faktycznie typu Post, a nie te z żadnych typów pochodnych. Różni się to od zapytania, które używa is metody lub OfType, która zwróci również wystąpienia dowolnych typów pochodnych. Rozważmy na przykład zapytanie:

var query = context.Posts.OfType<Post>();

Co przekłada się na inny język SQL:

      SELECT [p].[Id], [p].[AuthorId], [p].[BlogId], [p].[Content], [p].[Discriminator], [p].[PublishedOn], [p].[Title], [p].[PromoText]
      FROM [Posts] AS [p]

I zwróci zarówno jednostki, jak Post i FeaturedPost .

Obsługa AT TIME ZONE

Napiwek

Pokazany tutaj kod pochodzi z pliku MiscellaneousTranslationsSample.cs.

Program EF7 wprowadza nowe AtTimeZone funkcje dla i DateTimeOffsetDateTime . Te funkcje tłumaczą się na AT TIME ZONE klauzule w wygenerowanych bazach SQL. Przykład:

var query = context.Posts
    .Select(
        post => new
        {
            post.Title,
            PacificTime = EF.Functions.AtTimeZone(post.PublishedOn, "Pacific Standard Time"),
            UkTime = EF.Functions.AtTimeZone(post.PublishedOn, "GMT Standard Time"),
        });

To zapytanie przekłada się na następujący kod SQL podczas korzystania z programu SQL Server:

SELECT [p].[Title], [p].[PublishedOn] AT TIME ZONE 'Pacific Standard Time' AS [PacificTime], [p].[PublishedOn] AT TIME ZONE 'GMT Standard Time' AS [UkTime]
FROM [Posts] AS [p]

Napiwek

Te tłumaczenia zostały zaimplementowane przez zespół dla programu SQL Server. W przypadku innych dostawców skontaktuj się z opiekunem dostawcy, aby dodać pomoc techniczną, jeśli został on wdrożony dla tego dostawcy.

Filtrowane dołączanie do ukrytych nawigacji

Napiwek

Pokazany tutaj kod pochodzi z pliku MiscellaneousTranslationsSample.cs.

Metody Include można teraz używać z EF.Property. Umożliwia to filtrowanie i porządkowanie nawet dla prywatnych właściwości nawigacji lub prywatnych nawigacji reprezentowanych przez pola. Przykład:

var query = context.Blogs.Include(
    blog => EF.Property<ICollection<Post>>(blog, "Posts")
        .Where(post => post.Content.Contains(".NET"))
        .OrderBy(post => post.Title));

Jest to odpowiednik:

var query = context.Blogs.Include(
    blog => Posts
        .Where(post => post.Content.Contains(".NET"))
        .OrderBy(post => post.Title));

Ale nie musi Blog.Posts być publicznie dostępny.

W przypadku korzystania z programu SQL Server oba powyższe zapytania tłumaczą się na:

SELECT [b].[Id], [b].[Name], [t].[Id], [t].[AuthorId], [t].[BlogId], [t].[Content], [t].[Discriminator], [t].[PublishedOn], [t].[Title], [t].[PromoText]
FROM [Blogs] AS [b]
LEFT JOIN (
    SELECT [p].[Id], [p].[AuthorId], [p].[BlogId], [p].[Content], [p].[Discriminator], [p].[PublishedOn], [p].[Title], [p].[PromoText]
    FROM [Posts] AS [p]
    WHERE [p].[Content] LIKE N'%.NET%'
) AS [t] ON [b].[Id] = [t].[BlogId]
ORDER BY [b].[Id], [t].[Title]

Tłumaczenie usługi Cosmos dla Regex.IsMatch

Napiwek

Pokazany tutaj kod pochodzi z pliku CosmosQueriesSample.cs.

Program EF7 obsługuje używanie Regex.IsMatch zapytań LINQ w usłudze Azure Cosmos DB. Przykład:

var containsInnerT = await context.Triangles
    .Where(o => Regex.IsMatch(o.Name, "[a-z]t[a-z]", RegexOptions.IgnoreCase))
    .ToListAsync();

Przekłada się na następujący kod SQL:

SELECT c
FROM root c
WHERE ((c["Discriminator"] = "Triangle") AND RegexMatch(c["Name"], "[a-z]t[a-z]", "i"))

Ulepszenia interfejsu API i zachowania dbContext

PROGRAM EF7 zawiera różne małe ulepszenia DbContext klas i powiązane.

Napiwek

Kod przykładów w tej sekcji pochodzi z pliku DbContextApiSample.cs.

Tłumik dla niezainicjowanych właściwości dbSet

Publiczne, ustawiane DbSet właściwości na obiekcie DbContext są automatycznie inicjowane przez program EF Core podczas DbContext konstruowania. Rozważmy na przykład następującą DbContext definicję:

public class SomeDbContext : DbContext
{
    public DbSet<Blog> Blogs { get; set; }
}

Właściwość Blogs zostanie ustawiona na DbSet<Blog> wystąpienie w ramach konstruowania DbContext wystąpienia. Dzięki temu kontekst może być używany dla zapytań bez żadnych dodatkowych kroków.

Jednak po wprowadzeniu typów odwołań dopuszczających wartość null w języku C# kompilator ostrzega teraz, że właściwość Blogs niezwiązana z wartością null nie została zainicjowana:

[CS8618] Non-nullable property 'Blogs' must contain a non-null value when exiting constructor. Consider declaring the property as nullable.

Jest to fałszywe ostrzeżenie; właściwość jest ustawiona na wartość inną niż null przez program EF Core. Ponadto deklarowanie właściwości jako dopuszczającej wartość null spowoduje, że ostrzeżenie zniknie, ale nie jest to dobry pomysł, ponieważ, koncepcyjnie, właściwość nie jest dopuszczana do wartości null i nigdy nie będzie mieć wartości null.

Program EF7 zawiera element DiagnosticSuppressor dla DbSet właściwości, DbContext które uniemożliwiają kompilatorowi wygenerowanie tego ostrzeżenia.

Napiwek

Ten wzorzec pochodzi z dni, w których właściwości automatyczne języka C# były bardzo ograniczone. W przypadku nowoczesnego języka C# rozważ utworzenie właściwości automatycznych tylko do odczytu, a następnie jawne zainicjowanie ich w konstruktorze DbContext lub uzyskanie buforowanego DbSet wystąpienia z kontekstu w razie potrzeby. Na przykład public DbSet<Blog> Blogs => Set<Blog>().

Odróżnianie anulowania od niepowodzenia w dziennikach

Czasami aplikacja jawnie anuluje zapytanie lub inną operację bazy danych. Zwykle odbywa się to przy użyciu metody przekazanej CancellationToken do metody wykonującej operację.

W programie EF Core 6 zdarzenia rejestrowane po anulowaniu operacji są takie same jak te rejestrowane, gdy operacja kończy się niepowodzeniem z jakiegoś innego powodu. Program EF7 wprowadza nowe zdarzenia dziennika przeznaczone specjalnie dla anulowanych operacji bazy danych. Te nowe zdarzenia są domyślnie rejestrowane na Debug poziomie. W poniższej tabeli przedstawiono odpowiednie zdarzenia i ich domyślne poziomy dziennika:

Zdarzenie opis Domyślny poziom dziennika
CoreEventId.QueryIterationFailed Wystąpił błąd podczas przetwarzania wyników zapytania. LogLevel.Error
CoreEventId.SaveChangesFailed Wystąpił błąd podczas próby zapisania zmian w bazie danych. LogLevel.Error
RelationalEventId.CommandError Wystąpił błąd podczas wykonywania polecenia bazy danych. LogLevel.Error
CoreEventId.QueryCanceled Zapytanie zostało anulowane. LogLevel.Debug
CoreEventId.SaveChangesCanceled Polecenie bazy danych zostało anulowane podczas próby zapisania zmian. LogLevel.Debug
RelationalEventId.CommandCanceled Wykonanie obiektu DbCommand zostało anulowane. LogLevel.Debug

Uwaga

Anulowanie jest wykrywane przez sprawdzenie wyjątku zamiast sprawdzania tokenu anulowania. Oznacza to, że anulowania nie są wyzwalane za pośrednictwem tokenu anulowania nadal będą wykrywane i rejestrowane w ten sposób.

Nowe IProperty i INavigation przeciążenia dla EntityEntry metod

Kod pracujący z modelem EF często ma IPropertyINavigation metadane właściwości lub nawigacji lub reprezentujące je. JednostkaEntry jest następnie używana do pobierania wartości właściwości/nawigacji lub wykonywania zapytania o jego stan. Jednak przed ef7, to wymaga przekazania nazwy właściwości lub nawigacji do metod EntityEntry, które następnie ponownie wyszukać IProperty lub INavigation. W programie EF7 IProperty element lub INavigation można przekazać bezpośrednio, unikając dodatkowego wyszukiwania.

Rozważmy na przykład metodę znalezienia wszystkich elementów równorzędnych danej jednostki:

public static IEnumerable<TEntity> FindSiblings<TEntity>(
    this DbContext context, TEntity entity, string navigationToParent)
    where TEntity : class
{
    var parentEntry = context.Entry(entity).Reference(navigationToParent);

    return context.Entry(parentEntry.CurrentValue!)
        .Collection(parentEntry.Metadata.Inverse!)
        .CurrentValue!
        .OfType<TEntity>()
        .Where(e => !ReferenceEquals(e, entity));
}

Ta metoda znajduje element nadrzędny danej jednostki, a następnie przekazuje odwrotność INavigation do Collection metody wpisu nadrzędnego. Te metadane są następnie używane do zwracania wszystkich elementów równorzędnych danego elementu nadrzędnego. Oto przykład użycia:


Console.WriteLine($"Siblings to {post.Id}: '{post.Title}' are...");
foreach (var sibling in context.FindSiblings(post, nameof(post.Blog)))
{
    Console.WriteLine($"    {sibling.Id}: '{sibling.Title}'");
}

Dane wyjściowe:

Siblings to 1: 'Announcing Entity Framework 7 Preview 7: Interceptors!' are...
    5: 'Productivity comes to .NET MAUI in Visual Studio 2022'
    6: 'Announcing .NET 7 Preview 7'
    7: 'ASP.NET Core updates in .NET 7 Preview 7'

EntityEntry dla typów jednostek typu współużytkowanego

Program EF Core może używać tego samego typu CLR dla wielu różnych typów jednostek. Są one nazywane typami jednostek typu współużytkowanego i są często używane do mapowania typu słownika z parami klucz/wartość używanymi dla właściwości typu jednostki. Na przykład BuildMetadata typ jednostki można zdefiniować bez definiowania dedykowanego typu CLR:

modelBuilder.SharedTypeEntity<Dictionary<string, object>>(
    "BuildMetadata", b =>
    {
        b.IndexerProperty<int>("Id");
        b.IndexerProperty<string>("Tag");
        b.IndexerProperty<Version>("Version");
        b.IndexerProperty<string>("Hash");
        b.IndexerProperty<bool>("Prerelease");
    });

Zwróć uwagę, że typ jednostki typu współużytkowanego musi mieć nazwę — w tym przypadku nazwa to BuildMetadata. Te typy jednostek są następnie używane przy użyciu DbSet typu jednostki, który jest uzyskiwany przy użyciu nazwy. Przykład:

public DbSet<Dictionary<string, object>> BuildMetadata
    => Set<Dictionary<string, object>>("BuildMetadata");

Może to DbSet służyć do śledzenia wystąpień jednostek:

await context.BuildMetadata.AddAsync(
    new Dictionary<string, object>
    {
        { "Tag", "v7.0.0-rc.1.22426.7" },
        { "Version", new Version(7, 0, 0) },
        { "Prerelease", true },
        { "Hash", "dc0f3e8ef10eb1464b27f0fd4704f53c01226036" }
    });

I wykonaj zapytania:

var builds = await context.BuildMetadata
    .Where(metadata => !EF.Property<bool>(metadata, "Prerelease"))
    .OrderBy(metadata => EF.Property<string>(metadata, "Tag"))
    .ToListAsync();

Teraz w programie EF7 istnieje również Entry metoda, na DbSet której można użyć do uzyskania stanu wystąpienia, nawet jeśli nie jest jeszcze śledzona. Przykład:

var state = context.BuildMetadata.Entry(build).State;

ContextInitialized jest teraz rejestrowane jako Debug

W programie EF7 zdarzenie ContextInitialized jest rejestrowane na Debug poziomie. Przykład:

dbug: 10/7/2022 12:27:52.379 CoreEventId.ContextInitialized[10403] (Microsoft.EntityFrameworkCore.Infrastructure)
      Entity Framework Core 7.0.0 initialized 'BlogsContext' using provider 'Microsoft.EntityFrameworkCore.SqlServer:7.0.0' with options: SensitiveDataLoggingEnabled using NetTopologySuite

W poprzednich wersjach była rejestrowana na Information poziomie. Przykład:

info: 10/7/2022 12:30:34.757 CoreEventId.ContextInitialized[10403] (Microsoft.EntityFrameworkCore.Infrastructure)
      Entity Framework Core 7.0.0 initialized 'BlogsContext' using provider 'Microsoft.EntityFrameworkCore.SqlServer:7.0.0' with options: SensitiveDataLoggingEnabled using NetTopologySuite

W razie potrzeby poziom dziennika można zmienić z powrotem na Information:

optionsBuilder.ConfigureWarnings(
    builder =>
    {
        builder.Log((CoreEventId.ContextInitialized, LogLevel.Information));
    });

IEntityEntryGraphIterator jest publicznie używany

W programie EF7 IEntityEntryGraphIterator usługa może być używana przez aplikacje. Jest to usługa używana wewnętrznie podczas odnajdywania grafu jednostek do śledzenia, a także przez TrackGraphelement . Oto przykład, który iteruje wszystkie jednostki osiągalne od pewnej jednostki początkowej:

var blogEntry = context.ChangeTracker.Entries<Blog>().First();
var found = new HashSet<object>();
var iterator = context.GetService<IEntityEntryGraphIterator>();
iterator.TraverseGraph(new EntityEntryGraphNode<HashSet<object>>(blogEntry, found, null, null), node =>
{
    if (node.NodeState.Contains(node.Entry.Entity))
    {
        return false;
    }

    Console.Write($"Found with '{node.Entry.Entity.GetType().Name}'");

    if (node.InboundNavigation != null)
    {
        Console.Write($" by traversing '{node.InboundNavigation.Name}' from '{node.SourceEntry!.Entity.GetType().Name}'");
    }

    Console.WriteLine();

    node.NodeState.Add(node.Entry.Entity);

    return true;
});

Console.WriteLine();
Console.WriteLine($"Finished iterating. Found {found.Count} entities.");
Console.WriteLine();

Uwaga:

  • Iterator zatrzymuje przechodzenie z danego węzła, gdy delegat wywołania zwrotnego zwraca wartość false. Ten przykład śledzi odwiedzone jednostki i zwraca informacje false o tym, kiedy jednostka została już odwiedzona. Zapobiega to nieskończonym pętlom wynikającym z cykli na wykresie.
  • Obiekt EntityEntryGraphNode<TState> umożliwia przekazanie stanu bez przechwytywania go do delegata.
  • Dla każdego odwiedzonego węzła innego niż pierwszy odnaleziono węzeł, z którego został odnaleziony, a nawigacja, za pomocą którego została odnaleziona, jest przekazywana do wywołania zwrotnego.

Ulepszenia tworzenia modelu

PROGRAM EF7 zawiera różne małe ulepszenia w tworzeniu modeli.

Napiwek

Kod przykładów w tej sekcji pochodzi z pliku ModelBuildingSample.cs.

Indeksy mogą być rosnące lub malejące

Domyślnie program EF Core tworzy indeksy rosnące. Program EF7 obsługuje również tworzenie indeksów malejących. Przykład:

modelBuilder
    .Entity<Post>()
    .HasIndex(post => post.Title)
    .IsDescending();

Możesz też użyć atrybutu Index mapowania:

[Index(nameof(Title), AllDescending = true)]
public class Post
{
    public int Id { get; set; }

    [MaxLength(64)]
    public string? Title { get; set; }
}

Jest to rzadko przydatne w przypadku indeksów w pojedynczej kolumnie, ponieważ baza danych może używać tego samego indeksu do porządkowania w obu kierunkach. Jednak nie jest tak w przypadku indeksów złożonych w wielu kolumnach, w których kolejność w każdej kolumnie może być ważna. Program EF Core obsługuje tę funkcję, umożliwiając wielu kolumnom definiowanie innej kolejności dla każdej kolumny. Przykład:

modelBuilder
    .Entity<Blog>()
    .HasIndex(blog => new { blog.Name, blog.Owner })
    .IsDescending(false, true);

Możesz też użyć atrybutu mapowania:

[Index(nameof(Name), nameof(Owner), IsDescending = new[] { false, true })]
public class Blog
{
    public int Id { get; set; }

    [MaxLength(64)]
    public string? Name { get; set; }

    [MaxLength(64)]
    public string? Owner { get; set; }

    public List<Post> Posts { get; } = new();
}

Spowoduje to wykonanie następujących czynności sql podczas korzystania z programu SQL Server:

CREATE INDEX [IX_Blogs_Name_Owner] ON [Blogs] ([Name], [Owner] DESC);

Na koniec można utworzyć wiele indeksów w tym samym uporządkowanym zestawie kolumn, podając nazwy indeksów. Przykład:

modelBuilder
    .Entity<Blog>()
    .HasIndex(blog => new { blog.Name, blog.Owner }, "IX_Blogs_Name_Owner_1")
    .IsDescending(false, true);

modelBuilder
    .Entity<Blog>()
    .HasIndex(blog => new { blog.Name, blog.Owner }, "IX_Blogs_Name_Owner_2")
    .IsDescending(true, true);

Możesz też użyć atrybutów mapowania:

[Index(nameof(Name), nameof(Owner), IsDescending = new[] { false, true }, Name = "IX_Blogs_Name_Owner_1")]
[Index(nameof(Name), nameof(Owner), IsDescending = new[] { true, true }, Name = "IX_Blogs_Name_Owner_2")]
public class Blog
{
    public int Id { get; set; }

    [MaxLength(64)]
    public string? Name { get; set; }

    [MaxLength(64)]
    public string? Owner { get; set; }

    public List<Post> Posts { get; } = new();
}

Spowoduje to wygenerowanie następującego kodu SQL w programie SQL Server:

CREATE INDEX [IX_Blogs_Name_Owner_1] ON [Blogs] ([Name], [Owner] DESC);
CREATE INDEX [IX_Blogs_Name_Owner_2] ON [Blogs] ([Name] DESC, [Owner] DESC);

Atrybut mapowania dla kluczy złożonych

Program EF7 wprowadza nowy atrybut mapowania (np. "adnotacja danych") do określania właściwości klucza podstawowego lub właściwości dowolnego typu jednostki. W przeciwieństwie do System.ComponentModel.DataAnnotations.KeyAttributeklasy , PrimaryKeyAttribute jest umieszczany w klasie typu jednostki, a nie we właściwości klucza. Przykład:

[PrimaryKey(nameof(PostKey))]
public class Post
{
    public int PostKey { get; set; }
}

Dzięki temu jest to naturalne dopasowanie do definiowania kluczy złożonych:

[PrimaryKey(nameof(PostId), nameof(CommentId))]
public class Comment
{
    public int PostId { get; set; }
    public int CommentId { get; set; }
    public string CommentText { get; set; } = null!;
}

Zdefiniowanie indeksu w klasie oznacza również, że może służyć do określania właściwości prywatnych lub pól jako kluczy, mimo że zwykle byłyby one ignorowane podczas kompilowania modelu EF. Przykład:

[PrimaryKey(nameof(_id))]
public class Tag
{
    private readonly int _id;
}

DeleteBehavior mapowanie atrybutu

Program EF7 wprowadza atrybut mapowania (np. "adnotację danych"), aby określić DeleteBehavior dla relacji. Na przykład wymagane relacje są tworzone DeleteBehavior.Cascade domyślnie. Można to zmienić domyślnie DeleteBehavior.NoAction przy użyciu polecenia DeleteBehaviorAttribute:

public class Post
{
    public int Id { get; set; }
    public string? Title { get; set; }

    [DeleteBehavior(DeleteBehavior.NoAction)]
    public Blog Blog { get; set; } = null!;
}

Spowoduje to wyłączenie usuwania kaskadowego dla relacji Wpisy w blogu.

Właściwości mapowane na różne nazwy kolumn

Niektóre wzorce mapowania powodują mapowanie tej samej właściwości CLR na kolumnę w każdej z wielu różnych tabel. Program EF7 umożliwia tym kolumnom różne nazwy. Rozważmy na przykład prostą hierarchię dziedziczenia:

public abstract class Animal
{
    public int Id { get; set; }
    public string Breed { get; set; } = null!;
}

public class Cat : Animal
{
    public string? EducationalLevel { get; set; }
}

public class Dog : Animal
{
    public string? FavoriteToy { get; set; }
}

Dzięki strategii mapowania dziedziczenia TPT te typy zostaną zamapowane na trzy tabele. Jednak kolumna klucza podstawowego w każdej tabeli może mieć inną nazwę. Przykład:

CREATE TABLE [Animals] (
    [Id] int NOT NULL IDENTITY,
    [Breed] nvarchar(max) NOT NULL,
    CONSTRAINT [PK_Animals] PRIMARY KEY ([Id])
);

CREATE TABLE [Cats] (
    [CatId] int NOT NULL,
    [EducationalLevel] nvarchar(max) NULL,
    CONSTRAINT [PK_Cats] PRIMARY KEY ([CatId]),
    CONSTRAINT [FK_Cats_Animals_CatId] FOREIGN KEY ([CatId]) REFERENCES [Animals] ([Id]) ON DELETE CASCADE
);

CREATE TABLE [Dogs] (
    [DogId] int NOT NULL,
    [FavoriteToy] nvarchar(max) NULL,
    CONSTRAINT [PK_Dogs] PRIMARY KEY ([DogId]),
    CONSTRAINT [FK_Dogs_Animals_DogId] FOREIGN KEY ([DogId]) REFERENCES [Animals] ([Id]) ON DELETE CASCADE
);

Program EF7 umożliwia skonfigurowanie tego mapowania przy użyciu konstruktora zagnieżdżonych tabel:

modelBuilder.Entity<Animal>().ToTable("Animals");

modelBuilder.Entity<Cat>()
    .ToTable(
        "Cats",
        tableBuilder => tableBuilder.Property(cat => cat.Id).HasColumnName("CatId"));

modelBuilder.Entity<Dog>()
    .ToTable(
        "Dogs",
        tableBuilder => tableBuilder.Property(dog => dog.Id).HasColumnName("DogId"));

Za pomocą mapowania Breed dziedziczenia TPC właściwość może być również mapowana na różne nazwy kolumn w różnych tabelach. Rozważmy na przykład następujące tabele TPC:

CREATE TABLE [Cats] (
    [CatId] int NOT NULL DEFAULT (NEXT VALUE FOR [AnimalSequence]),
    [CatBreed] nvarchar(max) NOT NULL,
    [EducationalLevel] nvarchar(max) NULL,
    CONSTRAINT [PK_Cats] PRIMARY KEY ([CatId])
);

CREATE TABLE [Dogs] (
    [DogId] int NOT NULL DEFAULT (NEXT VALUE FOR [AnimalSequence]),
    [DogBreed] nvarchar(max) NOT NULL,
    [FavoriteToy] nvarchar(max) NULL,
    CONSTRAINT [PK_Dogs] PRIMARY KEY ([DogId])
);

Program EF7 obsługuje to mapowanie tabeli:

modelBuilder.Entity<Animal>().UseTpcMappingStrategy();

modelBuilder.Entity<Cat>()
    .ToTable(
        "Cats",
        builder =>
        {
            builder.Property(cat => cat.Id).HasColumnName("CatId");
            builder.Property(cat => cat.Breed).HasColumnName("CatBreed");
        });

modelBuilder.Entity<Dog>()
    .ToTable(
        "Dogs",
        builder =>
        {
            builder.Property(dog => dog.Id).HasColumnName("DogId");
            builder.Property(dog => dog.Breed).HasColumnName("DogBreed");
        });

Jednokierunkowe relacje wiele-do-wielu

Program EF7 obsługuje relacje wiele-do-wielu, w których jedna lub druga nie ma właściwości nawigacji. Rozważmy na przykład Post typy i Tag :

public class Post
{
    public int Id { get; set; }
    public string? Title { get; set; }
    public Blog Blog { get; set; } = null!;
    public List<Tag> Tags { get; } = new();
}
public class Tag
{
    public int Id { get; set; }
    public string TagName { get; set; } = null!;
}

Zwróć uwagę, że Post typ ma właściwość nawigacji dla listy tagów, ale Tag typ nie ma właściwości nawigacji dla wpisów. W programie EF7 można to nadal skonfigurować jako relację wiele-do-wielu, umożliwiając użycie tego samego Tag obiektu dla wielu różnych wpisów. Przykład:

modelBuilder
    .Entity<Post>()
    .HasMany(post => post.Tags)
    .WithMany();

Spowoduje to mapowanie na odpowiednią tabelę sprzężenia:

CREATE TABLE [Tags] (
    [Id] int NOT NULL IDENTITY,
    [TagName] nvarchar(max) NOT NULL,
    CONSTRAINT [PK_Tags] PRIMARY KEY ([Id])
);

CREATE TABLE [Posts] (
    [Id] int NOT NULL IDENTITY,
    [Title] nvarchar(64) NULL,
    [BlogId] int NOT NULL,
    CONSTRAINT [PK_Posts] PRIMARY KEY ([Id]),
    CONSTRAINT [FK_Posts_Blogs_BlogId] FOREIGN KEY ([BlogId]) REFERENCES [Blogs] ([Id])
);

CREATE TABLE [PostTag] (
    [PostId] int NOT NULL,
    [TagsId] int 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
);

A relacja może być używana jako wiele-do-wielu w normalny sposób. Na przykład wstawianie niektórych wpisów, które współużytkuje różne tagi z wspólnego zestawu:

var tags = new Tag[] { new() { TagName = "Tag1" }, new() { TagName = "Tag2" }, new() { TagName = "Tag2" }, };

await context.AddRangeAsync(new Blog { Posts =
{
    new Post { Tags = { tags[0], tags[1] } },
    new Post { Tags = { tags[1], tags[0], tags[2] } },
    new Post()
} });

await context.SaveChangesAsync();

Dzielenie jednostki

Dzielenie jednostek mapuje jeden typ jednostki na wiele tabel. Rozważmy na przykład bazę danych z trzema tabelami, które przechowują dane klienta:

  • Tabela Customers informacji o klientach
  • Tabela PhoneNumbers numeru telefonu klienta
  • Addresses Tabela adresu klienta

Poniżej przedstawiono definicje tych tabel w programie SQL Server:

CREATE TABLE [Customers] (
    [Id] int NOT NULL IDENTITY,
    [Name] nvarchar(max) NOT NULL,
    CONSTRAINT [PK_Customers] PRIMARY KEY ([Id])
);
    
CREATE TABLE [PhoneNumbers] (
    [CustomerId] int NOT NULL,
    [PhoneNumber] nvarchar(max) NULL,
    CONSTRAINT [PK_PhoneNumbers] PRIMARY KEY ([CustomerId]),
    CONSTRAINT [FK_PhoneNumbers_Customers_CustomerId] FOREIGN KEY ([CustomerId]) REFERENCES [Customers] ([Id]) ON DELETE CASCADE
);

CREATE TABLE [Addresses] (
    [CustomerId] int NOT NULL,
    [Street] nvarchar(max) NOT NULL,
    [City] nvarchar(max) NOT NULL,
    [PostCode] nvarchar(max) NULL,
    [Country] nvarchar(max) NOT NULL,
    CONSTRAINT [PK_Addresses] PRIMARY KEY ([CustomerId]),
    CONSTRAINT [FK_Addresses_Customers_CustomerId] FOREIGN KEY ([CustomerId]) REFERENCES [Customers] ([Id]) ON DELETE CASCADE
);

Każda z tych tabel jest zwykle mapowana na własny typ jednostki z relacjami między typami. Jeśli jednak wszystkie trzy tabele są zawsze używane razem, bardziej wygodne może być mapowania ich wszystkich na pojedynczy typ jednostki. Przykład:

public class Customer
{
    public Customer(string name, string street, string city, string? postCode, string country)
    {
        Name = name;
        Street = street;
        City = city;
        PostCode = postCode;
        Country = country;
    }

    public int Id { get; set; }
    public string Name { get; set; }
    public string? PhoneNumber { get; set; }
    public string Street { get; set; }
    public string City { get; set; }
    public string? PostCode { get; set; }
    public string Country { get; set; }
}

Jest to osiągane w programie EF7 przez wywołanie SplitToTable każdego podziału w typie jednostki. Na przykład poniższy kod dzieli Customer typ jednostki na Customerstabele , PhoneNumbersi Addresses pokazane powyżej:

modelBuilder.Entity<Customer>(
    entityBuilder =>
    {
        entityBuilder
            .ToTable("Customers")
            .SplitToTable(
                "PhoneNumbers",
                tableBuilder =>
                {
                    tableBuilder.Property(customer => customer.Id).HasColumnName("CustomerId");
                    tableBuilder.Property(customer => customer.PhoneNumber);
                })
            .SplitToTable(
                "Addresses",
                tableBuilder =>
                {
                    tableBuilder.Property(customer => customer.Id).HasColumnName("CustomerId");
                    tableBuilder.Property(customer => customer.Street);
                    tableBuilder.Property(customer => customer.City);
                    tableBuilder.Property(customer => customer.PostCode);
                    tableBuilder.Property(customer => customer.Country);
                });
    });

Należy również zauważyć, że w razie potrzeby można określić różne nazwy kolumn klucza podstawowego dla każdej z tabel.

Ciągi UTF-8 programu SQL Server

Ciągi Unicode programu SQL Server reprezentowane przez nchar typy danych i nvarchar są przechowywane jako UTF-16. Ponadto char typy danych i varchar służą do przechowywania ciągów innych niż Unicode z obsługą różnych zestawów znaków.

Począwszy od programu SQL Server 2019, char typy danych i varchar mogą zamiast tego przechowywać ciągi Unicode z kodowaniem UTF-8 . Element jest osiągany przez ustawienie jednego z sortowania UTF-8. Na przykład poniższy kod konfiguruje zmienną długość ciągu UTF-8 programu SQL Server dla kolumny CommentText :

modelBuilder
    .Entity<Comment>()
    .Property(comment => comment.CommentText)
    .HasColumnType("varchar(max)")
    .UseCollation("LATIN1_GENERAL_100_CI_AS_SC_UTF8");

Ta konfiguracja generuje następującą definicję kolumny programu SQL Server:

CREATE TABLE [Comment] (
    [PostId] int NOT NULL,
    [CommentId] int NOT NULL,
    [CommentText] varchar(max) COLLATE LATIN1_GENERAL_100_CI_AS_SC_UTF8 NOT NULL,
    CONSTRAINT [PK_Comment] PRIMARY KEY ([PostId], [CommentId])
);

Tabele czasowe obsługują należące do nich jednostki

Mapowanie tabel czasowych programu EF Core SQL Server zostało ulepszone w programie EF7 w celu obsługi udostępniania tabel. W szczególności domyślne mapowanie dla należących do nich pojedynczych jednostek używa udostępniania tabel.

Rozważmy na przykład typ Employee jednostki właściciela i jej typ jednostki EmployeeInfonależącej do niego:

public class Employee
{
    public Guid EmployeeId { get; set; }
    public string Name { get; set; } = null!;

    public EmployeeInfo Info { get; set; } = null!;
}

public class EmployeeInfo
{
    public string Position { get; set; } = null!;
    public string Department { get; set; } = null!;
    public string? Address { get; set; }
    public decimal? AnnualSalary { get; set; }
}

Jeśli te typy są mapowane na tę samą tabelę, w programie EF7 ta tabela może być tabelą czasową:

modelBuilder
    .Entity<Employee>()
    .ToTable(
        "Employees",
        tableBuilder =>
        {
            tableBuilder.IsTemporal();
            tableBuilder.Property<DateTime>("PeriodStart").HasColumnName("PeriodStart");
            tableBuilder.Property<DateTime>("PeriodEnd").HasColumnName("PeriodEnd");
        })
    .OwnsOne(
        employee => employee.Info,
        ownedBuilder => ownedBuilder.ToTable(
            "Employees",
            tableBuilder =>
            {
                tableBuilder.IsTemporal();
                tableBuilder.Property<DateTime>("PeriodStart").HasColumnName("PeriodStart");
                tableBuilder.Property<DateTime>("PeriodEnd").HasColumnName("PeriodEnd");
            }));

Uwaga

Ułatwianie tej konfiguracji jest śledzone przez problem nr 29303. Zagłosuj na ten problem, jeśli jest to coś, co chcesz zobaczyć zaimplementowane.

Ulepszone generowanie wartości

Program EF7 zawiera dwie znaczące ulepszenia automatycznego generowania wartości dla kluczowych właściwości.

Napiwek

Kod przykładów w tej sekcji pochodzi z pliku ValueGenerationSample.cs.

Generowanie wartości dla chronionych typów DDD

W projekcie opartym na domenie (DDD) "klucze chronione" mogą poprawić bezpieczeństwo typu kluczowych właściwości. Jest to osiągane przez zawijanie typu klucza w innym typie, który jest specyficzny dla użycia klucza. Na przykład poniższy kod definiuje ProductId typ kluczy produktów i CategoryId typ kluczy kategorii.

public readonly struct ProductId
{
    public ProductId(int value) => Value = value;
    public int Value { get; }
}

public readonly struct CategoryId
{
    public CategoryId(int value) => Value = value;
    public int Value { get; }
}

Są one następnie używane w Product typach jednostek i Category :

public class Product
{
    public Product(string name) => Name = name;
    public ProductId Id { get; set; }
    public string Name { get; set; }
    public CategoryId CategoryId { get; set; }
    public Category Category { get; set; } = null!;
}

public class Category
{
    public Category(string name) => Name = name;
    public CategoryId Id { get; set; }
    public string Name { get; set; }
    public List<Product> Products { get; } = new();
}

Uniemożliwia to przypadkowe przypisanie identyfikatora kategorii do produktu lub odwrotnie.

Ostrzeżenie

Podobnie jak w przypadku wielu koncepcji DDD, to ulepszone bezpieczeństwo typów wiąże się z kosztem dodatkowej złożoności kodu. Warto rozważyć, czy na przykład przypisanie identyfikatora produktu do kategorii jest czymś, co kiedykolwiek może się zdarzyć. Utrzymywanie prostych rzeczy może być ogólnie bardziej korzystne dla bazy kodu.

Chronione typy kluczy pokazane tutaj oba zawijają int wartości kluczy, co oznacza, że wartości całkowite będą używane w zamapowanych tabelach bazy danych. Jest to osiągane przez zdefiniowanie konwerterów wartości dla typów:

protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
{
    configurationBuilder.Properties<ProductId>().HaveConversion<ProductIdConverter>();
    configurationBuilder.Properties<CategoryId>().HaveConversion<CategoryIdConverter>();
}

private class ProductIdConverter : ValueConverter<ProductId, int>
{
    public ProductIdConverter()
        : base(v => v.Value, v => new(v))
    {
    }
}

private class CategoryIdConverter : ValueConverter<CategoryId, int>
{
    public CategoryIdConverter()
        : base(v => v.Value, v => new(v))
    {
    }
}

Uwaga

W tym miejscu kod używa struct typów. Oznacza to, że mają odpowiednie semantyki typu wartości do użycia jako klucze. Jeśli class zamiast tego są używane typy, należy zastąpić semantyka równości lub określić porównanie wartości.

W programie EF7 typy kluczy oparte na konwerterach wartości mogą używać automatycznie generowanych wartości kluczy, o ile podstawowy typ obsługuje ten typ. Jest to skonfigurowane w normalny sposób przy użyciu polecenia ValueGeneratedOnAdd:

modelBuilder.Entity<Product>().Property(product => product.Id).ValueGeneratedOnAdd();
modelBuilder.Entity<Category>().Property(category => category.Id).ValueGeneratedOnAdd();

Domyślnie powoduje to użycie kolumn w IDENTITY przypadku użycia z programem SQL Server:

CREATE TABLE [Categories] (
    [Id] int NOT NULL IDENTITY,
    [Name] nvarchar(max) NOT NULL,
    CONSTRAINT [PK_Categories] PRIMARY KEY ([Id]));

CREATE TABLE [Products] (
    [Id] int NOT NULL IDENTITY,
    [Name] nvarchar(max) NOT NULL,
    [CategoryId] int NOT NULL,
    CONSTRAINT [PK_Products] PRIMARY KEY ([Id]),
    CONSTRAINT [FK_Products_Categories_CategoryId] FOREIGN KEY ([CategoryId]) REFERENCES [Categories] ([Id]) ON DELETE CASCADE);

Które są używane w normalny sposób do generowania wartości kluczy podczas wstawiania jednostek:

MERGE [Categories] USING (
VALUES (@p0, 0),
(@p1, 1)) AS i ([Name], _Position) ON 1=0
WHEN NOT MATCHED THEN
INSERT ([Name])
VALUES (i.[Name])
OUTPUT INSERTED.[Id], i._Position;

Generowanie kluczy opartych na sekwencjach dla programu SQL Server

Program EF Core obsługuje generowanie wartości klucza przy użyciu kolumn programu SQL Server IDENTITY lub wzorzec Hi-Lo oparty na blokach kluczy generowanych przez sekwencję bazy danych. Program EF7 wprowadza obsługę sekwencji bazy danych dołączonej do domyślnego ograniczenia kolumny klucza. W najprostszej formie wymaga to jedynie poinformowania platformy EF Core o użyciu sekwencji dla właściwości klucza:

modelBuilder.Entity<Product>().Property(product => product.Id).UseSequence();

Spowoduje to zdefiniowanie sekwencji w bazie danych:

CREATE SEQUENCE [ProductSequence] START WITH 1 INCREMENT BY 1 NO MINVALUE NO MAXVALUE NO CYCLE;

Który jest następnie używany w domyślnym ograniczeniu kolumny klucza:

CREATE TABLE [Products] (
    [Id] int NOT NULL DEFAULT (NEXT VALUE FOR [ProductSequence]),
    [Name] nvarchar(max) NOT NULL,
    [CategoryId] int NOT NULL,
    CONSTRAINT [PK_Products] PRIMARY KEY ([Id]),
    CONSTRAINT [FK_Products_Categories_CategoryId] FOREIGN KEY ([CategoryId]) REFERENCES [Categories] ([Id]) ON DELETE CASCADE);

Uwaga

Ta forma generowania kluczy jest używana domyślnie do generowania kluczy w hierarchiach typów jednostek przy użyciu strategii mapowania TPC.

W razie potrzeby sekwencja może mieć inną nazwę i schemat. Przykład:

modelBuilder
    .Entity<Product>()
    .Property(product => product.Id)
    .UseSequence("ProductsSequence", "northwind");

Dalsza konfiguracja sekwencji jest tworzona przez jawne skonfigurowanie jej w modelu. Przykład:

modelBuilder
    .HasSequence<int>("ProductsSequence", "northwind")
    .StartsAt(1000)
    .IncrementsBy(2);

Ulepszenia narzędzi migracji

Program EF7 zawiera dwie znaczące ulepszenia w przypadku korzystania z narzędzi wiersza polecenia migracji platformy EF Core.

UseSqlServer itp. zaakceptuj wartość null

Bardzo często odczytuje parametry połączenia z pliku konfiguracji, a następnie przekazuje ten parametry połączenia do UseSqlServermetody , UseSqlitelub równoważnej metody dla innego dostawcy. Przykład:

services.AddDbContext<BloggingContext>(options =>
    options.UseSqlServer(Configuration.GetConnectionString("BloggingDatabase")));

Podczas stosowania migracji często zdarza się również przekazać parametry połączenia. Przykład:

dotnet ef database update --connection "Server=(localdb)\mssqllocaldb;Database=MyAppDb"

Lub w przypadku korzystania z pakietu Migracje.

./bundle.exe --connection "Server=(localdb)\mssqllocaldb;Database=MyAppDb"

W takim przypadku, mimo że parametry połączenia odczyt z konfiguracji nie jest używany, kod uruchamiania aplikacji nadal próbuje odczytać go z konfiguracji i przekazać go do UseSqlServer. Jeśli konfiguracja jest niedostępna, spowoduje to przekazanie wartości null do UseSqlServer. W programie EF7 jest to dozwolone, o ile parametry połączenia zostanie ostatecznie ustawiona później, na przykład przez przekazanie --connection do narzędzia wiersza polecenia.

Uwaga

Ta zmiana została wprowadzona dla UseSqlServer i UseSqlite. W przypadku innych dostawców skontaktuj się z opiekunem dostawcy, aby wprowadzić równoważną zmianę, jeśli nie została jeszcze wykonana dla tego dostawcy.

Wykrywanie, kiedy narzędzia są uruchomione

Program EF Core uruchamia kod aplikacji, gdy dotnet-ef są używane polecenia programu PowerShell lub . Czasami może być konieczne wykrycie tej sytuacji, aby zapobiec wykonywaniu nieodpowiedniego kodu w czasie projektowania. Na przykład kod, który automatycznie stosuje migracje podczas uruchamiania, prawdopodobnie nie powinien tego robić w czasie projektowania. W programie EF7 można to wykryć przy użyciu flagi EF.IsDesignTime :

if (!EF.IsDesignTime)
{
    await context.Database.MigrateAsync();
}

Program EF Core ustawia wartość IsDesignTime na true wartość , gdy kod aplikacji jest uruchamiany w imieniu narzędzi.

Ulepszenia wydajności dla serwerów proxy

Program EF Core obsługuje dynamicznie generowane serwery proxy na potrzeby ładowania leniwego i śledzenia zmian. Program EF7 zawiera dwie ulepszenia wydajności podczas korzystania z tych serwerów proxy:

  • Typy serwerów proxy są teraz tworzone leniwie. Oznacza to, że początkowy czas kompilowania modelu w przypadku korzystania z serwerów proxy może być znacznie szybszy w przypadku programu EF7 niż w przypadku programu EF Core 6.0.
  • Serwery proxy można teraz używać z skompilowanymi modelami.

Poniżej przedstawiono kilka wyników wydajności modelu z 449 typami jednostek, właściwościami 6390 i relacjami 720.

Scenariusz Metoda Średnia Błąd StdDev
Program EF Core 6.0 bez serwerów proxy TimeToFirstQuery 1,085 s 0,0083 s 0,0167 s
Program EF Core 6.0 z serwerami proxy śledzenia zmian TimeToFirstQuery 13.01 s 0,2040 s 0,4110 s
Program EF Core 7.0 bez serwerów proxy TimeToFirstQuery 1.442 s 0,0134 s 0,0272 s
Program EF Core 7.0 z serwerami proxy śledzenia zmian TimeToFirstQuery 1,446 s 0,0160 s 0,0323 s
Program EF Core 7.0 z serwerami proxy śledzenia zmian i skompilowanym modelem TimeToFirstQuery 0,162 s 0,0062 s 0,0125 s

W takim przypadku model z serwerami proxy śledzenia zmian może być gotowy do wykonania pierwszego zapytania 80 razy szybciej w programie EF7, niż było to możliwe w przypadku programu EF Core 6.0.

Powiązanie danych formularzy Windows Forms pierwszej klasy

Zespół windows Forms wprowadza kilka wielkich ulepszeń środowiska Projektant programu Visual Studio. Obejmuje to nowe środowiska powiązań danych, które dobrze integrują się z platformą EF Core.

Krótko mówiąc, nowe środowisko udostępnia program Visual Studio U.I. do tworzenia elementu ObjectDataSource:

Choose Category data source type

Można to następnie powiązać z platformą EF Core DbSet za pomocą prostego kodu:

public partial class MainForm : Form
{
    private ProductsContext? dbContext;

    public MainForm()
    {
        InitializeComponent();
    }

    protected override void OnLoad(EventArgs e)
    {
        base.OnLoad(e);

        this.dbContext = new ProductsContext();

        this.dbContext.Categories.Load();
        this.categoryBindingSource.DataSource = dbContext.Categories.Local.ToBindingList();
    }

    protected override void OnClosing(CancelEventArgs e)
    {
        base.OnClosing(e);

        this.dbContext?.Dispose();
        this.dbContext = null;
    }
}

Zobacz Wprowadzenie do formularzy systemu Windows, aby zapoznać się z kompletnym przewodnikiem i pobrać przykładową aplikację WinForms.