Sdílet prostřednictvím


Novinky v EF Core 7.0

EF Core 7.0 (EF7) byl vydán v listopadu 2022.

Tip

Ukázky můžete spustit a ladit stažením ukázkového kódu z GitHubu. Každý oddíl odkazuje na zdrojový kód specifický pro tento oddíl.

EF7 cílí na .NET 6 a lze ho použít s .NET 6 (LTS) nebo .NET 7.

Ukázkový model

Mnoho příkladů níže používá jednoduchý model s blogy, příspěvky, značkami a autory:

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

Některé z příkladů také používají agregační typy, které jsou mapovány různými způsoby v různých vzorcích. Pro kontakty existuje jeden agregační typ:

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

A druhý agregační typ pro post metadata:

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

Tip

Ukázkový model najdete v BlogsContext.cs.

Sloupce JSON

Většina relačních databází podporuje sloupce, které obsahují dokumenty JSON. Json v těchto sloupcích můžete procházet pomocí dotazů. To umožňuje například filtrování a řazení podle prvků dokumentů a projekce prvků z dokumentů do výsledků. Sloupce JSON umožňují relačním databázím převzít některé vlastnosti databází dokumentů a vytvořit tak užitečné hybridní propojení mezi těmito dvěma databázemi.

EF7 obsahuje podporu sloupců JSON nezávislou na poskytovateli s implementací sql Serveru. Tato podpora umožňuje mapováníagch U agregací je možné použít normální dotazy LINQ a tyto dotazy se přeloží do příslušných konstruktorů dotazů potřebných k přechodu do formátu JSON. EF7 také podporuje aktualizaci a ukládání změn do dokumentů JSON.

Poznámka:

Podpora SQLite pro JSON se plánuje pro post EF7. Poskytovatelé PostgreSQL a Pomelo MySQL už obsahují podporu sloupců JSON. Budeme spolupracovat s autory těchto poskytovatelů na sladění podpory JSON napříč všemi poskytovateli.

Mapování na sloupce JSON

V EF Core jsou agregační typy definovány pomocí OwnsOne a OwnsMany. Představte si například agregační typ z našeho ukázkového modelu, který se používá k ukládání kontaktních údajů:

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

To se pak dá použít v typu entity vlastníka, například k uložení kontaktních údajů autora:

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

Agregační typ se konfiguruje OnModelCreating pomocí OwnsOne:

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

Tip

Zde uvedený kód pochází z JsonColumnsSample.cs.

Ve výchozím nastavení zprostředkovatelé relačních databází mapují agregační typy podobné této tabulce jako vlastnící typ entity. To znamená, že každá vlastnost ContactDetails a Address třídy jsou mapovány na sloupec v Authors tabulce.

Někteří uložení autoři s kontaktními údaji budou vypadat takto:

Autoři

Id Name Contact_Address_Street Contact_Address_City Contact_Address_Postcode Contact_Address_Country Contact_Phone
0 Maddy Montaquila 1 Hlavní st Camberwick Green CW1 5ZH Velká Británie 01632 12345
2 Jeremy Likness 2 Hlavní st Chigley CW1 5ZH Velká Británie 01632 12346
3 Daniel Roth 3 Hlavní st Camberwick Green CW1 5ZH Velká Británie 01632 12347
4 Arthur Vickers 15a Main St Chigley CW1 5ZH Spojené království 01632 22345
5 Brice Lambson 4 Hlavní st Chigley CW1 5ZH Velká Británie 01632 12349

V případě potřeby je možné každý typ entity, který tvoří agregaci, namapovat na vlastní tabulku:

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

Stejná data se pak ukládají napříč třemi tabulkami:

Autoři

Id Name
0 Maddy Montaquila
2 Jeremy Likness
3 Daniel Roth
4 Arthur Vickers
5 Brice Lambson

Kontakty

AuthorId Telefon
0 01632 12345
2 01632 12346
3 01632 12347
4 01632 22345
5 01632 12349

Adresy

ContactDetailsAuthorId Ulice City PSČ Země
0 1 Hlavní st Camberwick Green CW1 5ZH Velká Británie
2 2 Hlavní st Chigley CW1 5ZH Velká Británie
3 3 Hlavní st Camberwick Green CW1 5ZH Velká Británie
4 15a Main St Chigley CW1 5ZH Spojené království
5 4 Hlavní st Chigley CW1 5ZH Velká Británie

Teď, pro zajímavou část. V EF7 ContactDetails je možné agregační typ namapovat na sloupec JSON. To vyžaduje pouze jedno volání ToJson() při konfiguraci agregovaného typu:

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

Tabulka Authors teď bude obsahovat sloupec JSON naplněný ContactDetails dokumentem JSON pro každého autora:

Autoři

Id Name Kontakt
0 Maddy Montaquila {
  "Telefon":"01632 12345",
  "Adresa": {
    "Město":"Camberwick Green",
    "Country":"UK",
    "Psč":"CW1 5ZH",
    "Ulice":"1 Hlavní st"
  }
}
2 Jeremy Likness {
  "Telefon":"01632 12346",
  "Adresa": {
    "Město":"Chigley",
    "Country":"UK",
    "Psč":"CH1 5ZH",
    "Ulice":"2 Main St"
  }
}
3 Daniel Roth {
  "Telefon":"01632 12347",
  "Adresa": {
    "Město":"Camberwick Green",
    "Country":"UK",
    "Psč":"CW1 5ZH",
    "Ulice":"3 Hlavní st"
  }
}
4 Arthur Vickers {
  "Telefon":"01632 12348",
  "Adresa": {
    "Město":"Chigley",
    "Country":"UK",
    "Psč":"CH1 5ZH",
    "Ulice":"15a Main St"
  }
}
5 Brice Lambson {
  "Telefon":"01632 12349",
  "Adresa": {
    "Město":"Chigley",
    "Country":"UK",
    "Psč":"CH1 5ZH",
    "Ulice":"4 Main St"
  }
}

Tip

Toto použití agregací je velmi podobné způsobu, jakým se dokumenty JSON mapují při použití zprostředkovatele EF Core pro Azure Cosmos DB. Sloupce JSON přinášejí možnosti použití EF Core proti databázím dokumentů do dokumentů vložených do relační databáze.

Výše uvedené dokumenty JSON jsou velmi jednoduché, ale tuto funkci mapování je možné použít i se složitějšími strukturami dokumentů. Představte si například jiný agregační typ z našeho ukázkového modelu, který se používá k reprezentaci metadat příspěvku:

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

Tento agregační typ obsahuje několik vnořených typů a kolekcí. Volání a OwnsOne OwnsMany slouží k mapování tohoto typu agregace:

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

Tip

ToJson k mapování celé agregace na dokument JSON je potřeba jenom u kořenového adresáře agregace.

S tímto mapováním může EF7 vytvořit a dotazovat do složitého dokumentu JSON, například takto:

{
  "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"
        }
      ]
    }
  ]
}

Poznámka:

Mapování prostorových typů přímo na JSON se zatím nepodporuje. Výše uvedený dokument používá double hodnoty jako alternativní řešení. Hlasujte pro podporu prostorových typů ve sloupcích JSON, pokud se jedná o něco, co vás zajímá.

Poznámka:

Mapování kolekcí primitivních typů na JSON se zatím nepodporuje. Výše uvedený dokument používá převaděč hodnot k transformaci kolekce na řetězec oddělený čárkami. Hlasujte pro Json: Pokud se jedná o něco, co vás zajímá, přidejte podporu pro kolekci primitivních typů .

Poznámka:

Mapování vlastněných typů na JSON se zatím nepodporuje ve spojení s dědičností TPT nebo TPC. Pokud se jedná o něco, co vás zajímá, hlasujte pro vlastnosti JSON s mapováním dědičnosti TPT/TPC.

Dotazy do sloupců JSON

Dotazy do sloupců JSON fungují stejně jako dotazování na jakýkoli jiný agregační typ EF Core. To znamená, že stačí použít LINQ! Zde je uvedeno několik příkladů.

Dotaz pro všechny autory, kteří žijí v Chigley:

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

Tento dotaz při použití SQL Serveru vygeneruje následující SQL:

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'

Všimněte si použití JSON_VALUE možnosti získat City z Address dokumentu JSON.

Select lze použít k extrahování a projektových prvků z dokumentu JSON:

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

Tento dotaz vygeneruje následující 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'

Tady je příklad, který v filtru a projekci dělá trochu víc a také objednávky podle telefonního čísla v dokumentu 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();

Tento dotaz vygeneruje následující 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 když dokument JSON obsahuje kolekce, můžete je projektovat ve výsledcích:

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();

Tento dotaz vygeneruje následující 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

Poznámka:

Složitější dotazy zahrnující kolekce JSON vyžadují jsonpath podporu. Pokud se jedná o něco, co vás zajímá, hlasujte pro podporu dotazování jsonpath.

Tip

Zvažte vytvoření indexů pro zlepšení výkonu dotazů v dokumentech JSON. Podívejte se například na data JSON indexu při použití SQL Serveru.

Aktualizace sloupců JSON

SaveChanges a SaveChangesAsync pracujte běžným způsobem, jak aktualizovat sloupec JSON. U rozsáhlých změn se aktualizuje celý dokument. Například nahrazení většiny Contact dokumentu pro 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();

V tomto případě se celý nový dokument předá 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']

Který se pak použije v UPDATE SQL:

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

Pokud se ale změní jenom dílčí dokument, EF Core použije JSON_MODIFY příkaz k aktualizaci pouze dílčího dokumentu. Například změna Address uvnitř 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();

Vygeneruje následující 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']

Který se používá v UPDATE volání JSON_MODIFY :

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

A konečně, pokud se změní pouze jedna vlastnost, EF Core znovu použije příkaz "JSON_MODIFY", tentokrát opraví pouze změněnou hodnotu vlastnosti. Příklad:

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

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

await context.SaveChangesAsync();

Vygeneruje následující 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']

Které se znovu používají s JSON_MODIFY:

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

ExecuteUpdate a ExecuteDelete (hromadné aktualizace)

EF Core ve výchozím nastavení sleduje změny entit a potom odesílá aktualizace do databáze , když je volána jedna z SaveChanges metod. Změny se odesílají pouze pro vlastnosti a relace, které se skutečně změnily. Sledované entity se také synchronizují se změnami odesílanými do databáze. Tento mechanismus představuje efektivní a pohodlný způsob, jak do databáze odesílat vložení, aktualizace a odstranění pro obecné účely. Tyto změny jsou také dávkové, aby se snížil počet doby odezvy databáze.

Někdy je ale užitečné spouštět příkazy aktualizace nebo odstraňování v databázi bez zásahu sledování změn. EF7 to umožňuje s novými ExecuteUpdate a ExecuteDelete metodami. Tyto metody se použijí na dotaz LINQ a na základě výsledků tohoto dotazu aktualizují nebo odstraní entity v databázi. Mnoho entit lze aktualizovat jedním příkazem a entity se nenačtou do paměti, což znamená, že to může vést k efektivnějším aktualizacím a odstranění.

Mějte však na paměti, že:

  • Konkrétní změny, které je třeba provést, musí být zadány explicitně; ef Core je automaticky nezjistí.
  • Všechny sledované entity se nebudou synchronizovat.
  • Další příkazy může být potřeba odeslat ve správném pořadí, aby nedošlo k porušení omezení databáze. Například odstranění závislých položek před odstraněním objektu zabezpečení.

To vše znamená, že stávající ExecuteUpdate mechanismus se místo nahrazení SaveChanges doplňují a ExecuteDelete metody doplňují.

Základní ExecuteDelete příklady

Tip

Zde uvedený kód pochází z ExecuteDeleteSample.cs.

Volání ExecuteDelete nebo ExecuteDeleteAsync okamžité DbSet odstranění všech entit DbSet z databáze. Pokud například chcete odstranit všechny Tag entity:

await context.Tags.ExecuteDeleteAsync();

Při použití SQL Serveru se spustí následující SQL:

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

Zajímavější je, že dotaz může obsahovat filtr. Příklad:

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

Provede se následující příkaz SQL:

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

Dotaz může také používat složitější filtry, včetně navigace na jiné typy. Pokud například chcete odstranit značky jenom ze starých blogových příspěvků:

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

Která se spustí:

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))

Základní ExecuteUpdate příklady

Tip

Zde uvedený kód pochází z ExecuteUpdateSample.cs.

ExecuteUpdate a ExecuteUpdateAsync chovají se velmi podobným způsobem jako ExecuteDelete metody. Hlavní rozdíl spočívá v tom, že aktualizace vyžaduje znalost vlastností, které se mají aktualizovat a jak je aktualizovat. Toho lze dosáhnout pomocí jednoho nebo více volání SetProperty. Chcete-li například aktualizovat Name každý blog:

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

První parametr SetProperty určuje, která vlastnost se má aktualizovat, v tomto případě Blog.Name. Druhý parametr určuje, jak se má nová hodnota vypočítat; v tomto případě tak, že vezme existující hodnotu a připojí "*Featured!*". Výsledný SQL je:

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

ExecuteDeleteStejně jako v případě , dotaz lze použít k filtrování entit, které se aktualizují. Kromě toho lze použít více volání SetProperty k aktualizaci více než jedné vlastnosti cílové entity. Například aktualizace Title všech Content příspěvků publikovaných před 2022:

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

V tomto případě je vygenerovaný SQL trochu složitější:

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

A konečně, stejně jako v případě ExecuteDelete, filtr může odkazovat na jiné tabulky. Pokud například chcete aktualizovat všechny značky ze starých příspěvků:

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

To vygeneruje:

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))

Další informace a ukázky kódu najdete ExecuteUpdate ExecuteDeletev tématu ExecuteUpdate a ExecuteDelete.

Dědičnost a více tabulek

ExecuteUpdate a ExecuteDelete může pracovat pouze s jednou tabulkou. To má vliv na práci s různými strategiemi mapování dědičnosti. Obecně platí, že při použití strategie mapování TPH nejsou žádné problémy, protože existuje pouze jedna tabulka, která se má upravit. Například odstranění všech FeaturedPost entit:

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

Při použití mapování TPH vygeneruje následující SQL:

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

Při použití strategie mapování TPC také nedochází k žádným problémům, protože jsou potřeba jenom změny v jedné tabulce:

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

Při pokusu o použití strategie mapování TPT se však nezdaří, protože by vyžadovalo odstranění řádků ze dvou různých tabulek.

Přidání filtru do dotazu často znamená, že operace selže se strategiemi TPC i TPT. To je opět proto, že řádky může být potřeba odstranit z více tabulek. Třeba tenhle dotaz:

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

Vygeneruje následující SQL při použití 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%')

Při použití TPC nebo TPT se ale nezdaří.

Tip

Problém č. 10879 sleduje přidání podpory automatického odesílání více příkazů v těchto scénářích. Hlasujte pro tento problém, pokud byste chtěli vidět implementované.

ExecuteDelete a vztahy

Jak je uvedeno výše, může být nutné odstranit nebo aktualizovat závislé entity před odstraněním objektu zabezpečení relace. Každý z nich Post je například závislý na jeho přidruženém Author. To znamená, že autor nelze odstranit, pokud na něj příspěvek stále odkazuje; tím dojde k porušení omezení cizího klíče v databázi. Například při pokusu o toto:

await context.Authors.ExecuteDeleteAsync();

Výsledkem bude následující výjimka na SQL Serveru:

Microsoft.Data.SqlClient.SqlException (0x80131904): Příkaz DELETE byl v konfliktu s omezením REFERENCE "FK_Posts_Authors_AuthorId". V databázi TphBlogsContext, tabulce dbo došlo ke konfliktu. Příspěvky", sloupec AuthorId. Příkaz byl ukončen.

Abychom to vyřešili, musíme nejprve odstranit příspěvky nebo oddělit vztah mezi jednotlivými příspěvky a jeho autor nastavením AuthorId vlastnosti cizího klíče na hodnotu null. Například pomocí možnosti odstranit:

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

Tip

TagWith lze použít k označení ExecuteDelete nebo ExecuteUpdate stejným způsobem jako značky normálních dotazů.

Výsledkem jsou dva samostatné příkazy; první odstranění závislých:

-- Deleting posts...

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

A druhý k odstranění objektů zabezpečení:

-- Deleting authors...

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

Důležité

Více ExecuteDelete příkazů ExecuteUpdate nebude ve výchozím nastavení obsaženo v jedné transakci. Rozhraní API transakcí DbContext lze však použít normálním způsobem, jak tyto příkazy zabalit do transakce.

Tip

Odesílání těchto příkazů v rámci jedné odezvy závisí na problému č. 10879. Hlasujte pro tento problém, pokud byste chtěli vidět implementované.

Konfigurace kaskádových odstranění v databázi může být velmi užitečná. V našem modelu je potřeba vztah mezi Blog a Post vyžaduje se, což způsobí, že EF Core nakonfiguruje kaskádové odstranění podle konvence. To znamená, že když se z databáze odstraní blog, odstraní se také všechny jeho závislé příspěvky. Pak následuje, že odstranit všechny blogy a příspěvky, které potřebujeme odstranit pouze blogy:

await context.Blogs.ExecuteDeleteAsync();

Výsledkem je následující SQL:

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

Protože odstraňuje blog, způsobí také odstranění všech souvisejících příspěvků nakonfigurovaným kaskádovým odstraněním.

Rychlejší saveChanges

V EF7 se výrazně zlepšil výkon SaveChanges a SaveChangesAsync byl výrazně vylepšen. V některých scénářích je teď ukládání změn až čtyřikrát rychlejší než u EF Core 6.0!

Většina z těchto vylepšení pochází z:

  • Provádění menšího počtu zaokrouhlování do databáze
  • Rychlejší generování SQL

Některé příklady těchto vylepšení jsou uvedené níže.

Tip

Zde uvedený kód pochází z SaveChangesPerformanceSample.cs.

Nepotřebné transakce jsou eliminovány.

Všechny moderní relační databáze zaručují transakční (většinu) jednoduchých příkazů SQL. To znamená, že příkaz nebude nikdy dokončen pouze částečně, i když dojde k chybě. EF7 zabraňuje spuštění explicitní transakce v těchto případech.

Podívejte se například na protokolování pro následující volání SaveChanges:

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

Ukazuje, že v EF Core 6.0 INSERT je příkaz zabalen pomocí příkazů, které mají začínat a pak potvrdit transakci:

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.

EF7 zjistí, že transakce zde není nutná, a proto odebere tato volání:

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

Tím se odeberou dvě zaokrouhlování databáze, což může mít obrovský rozdíl v celkovém výkonu, zejména když je vysoká latence volání do databáze. V typických produkčních systémech není databáze společně umístěná na stejném počítači jako aplikace. To znamená, že latence je často relativně vysoká, takže tato optimalizace je zvlášť efektivní v reálných produkčních systémech.

Vylepšené SQL pro vložení jednoduché identity

Výše uvedený případ vloží jeden řádek se sloupcem IDENTITY klíče a žádnými dalšími hodnotami generovanými databází. EF7 v tomto případě zjednodušuje SQL pomocí OUTPUT INSERTED. I když toto zjednodušení není platné pro mnoho dalších případů, je stále důležité zlepšit, protože tento druh vkládání s jedním řádkem je v mnoha aplikacích velmi běžný.

Vložení více řádků

V EF Core 6.0 byl výchozí přístup pro vkládání více řádků řízený omezeními v SQL Serveru pro tabulky s triggery. Chtěli jsme zajistit, aby výchozí prostředí fungovalo i pro menšinu uživatelů s triggery v jejich tabulkách. To znamená, že jsme nemohli použít jednoduchou OUTPUT klauzuli, protože na SQL Serveru to nefunguje s triggery. Místo toho při vkládání více entit EF Core 6.0 vygeneroval poměrně konvolutovaný SQL. Toto volání SaveChangesnapříklad:

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

await context.SaveChangesAsync();

Výsledkem jsou následující akce při spuštění na SQL Serveru s 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.

Důležité

I když je to složité, dávkové vkládání podobných vložení je stále výrazně rychlejší než odeslání jednoho příkazu pro každé vložení.

V EF7 můžete tento SQL získat i v případě, že tabulky obsahují triggery, ale pro běžný případ, kdy teď generujeme mnohem efektivnější příkazy, pokud jsou ještě poněkud složité, příkazy:

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;

Transakce je pryč, stejně jako v jediném případu vložení, protože MERGE je jediný příkaz chráněný implicitní transakcí. Dočasná tabulka je také pryč a klauzule OUTPUT teď odesílá vygenerovaná ID přímo klientovi. To může být čtyřikrát rychlejší než u EF Core 6.0 v závislosti na faktorech prostředí, jako je latence mezi aplikací a databází.

Aktivační události

Pokud tabulka obsahuje triggery, vyvolá se volání SaveChanges ve výše uvedeném kódu výjimku:

Neošetřená výjimka. Microsoft.EntityFrameworkCore.DbUpdateException:
Změny nelze uložit, protože cílová tabulka obsahuje triggery databáze. Odpovídajícím způsobem nakonfigurujte typ entity. Další informace najdete v tématu https://aka.ms/efcore-docs-sqlserver-save-changes-and-triggers .
>--- Microsoft.Data.SqlClient.SqlException (0x80131904):
Cílová tabulka BlogsWithTriggers příkazu DML nemůže mít žádné povolené triggery, pokud příkaz obsahuje klauzuli OUTPUT bez klauzule INTO.

Následující kód lze použít k informování EF Core, že tabulka má aktivační událost:

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

EF7 se pak při odesílání příkazů pro vložení a aktualizaci pro tuto tabulku vrátí k EF Core 6.0 SQL.

Další informace, včetně konvence pro automatickou konfiguraci všech mapovaných tabulek s triggery, najdete v tématu Tabulky SQL Serveru s triggery, které teď vyžadují speciální konfiguraci EF Core v dokumentaci k zásadním změnám EF7.

Méně zaokrouhlování pro vkládání grafů

Zvažte vložení grafu entit obsahujících novou hlavní entitu a také nové závislé entity s cizími klíči, které odkazují na nový objekt zabezpečení. Příklad:

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

Pokud je primární klíč objektu zabezpečení vygenerován databází, není hodnota nastavená pro cizí klíč v závislém objektu známa, dokud se objekt zabezpečení nevloží. EF Core vygeneruje dvě zaokrouhlovací rutiny pro tento objekt– jeden pro vložení objektu zabezpečení a vrácení nového primárního klíče a sekundu pro vložení závislých hodnot se sadou hodnot cizího klíče. A vzhledem k tomu, že existují dva příkazy pro toto, je potřeba transakce, což znamená, že jsou celkem čtyři zaokrouhlování:

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.

V některých případech je však hodnota primárního klíče známa před vložením objektu zabezpečení. Sem patří:

  • Hodnoty klíčů, které se negenerují automaticky
  • Hodnoty klíčů vygenerované v klientovi, například Guid klíče
  • Klíčové hodnoty, které se generují na serveru v dávkách, například při použití generátoru hodnot hi-lo

V EF7 jsou teď tyto případy optimalizované na jednu zpáteční cestu. Například ve výše uvedeném případě na SQL Serveru je možné primární klíč nakonfigurovat tak, Blog.Id aby používal strategii hi-lo generation:

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

Volání SaveChanges shora je teď optimalizované na jedno zaokrouhlování pro vložení.

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.

Všimněte si, že tady je stále potřeba transakce. Je to proto, že vložení se vytváří do dvou samostatných tabulek.

EF7 také používá jednu dávku v jiných případech, kdy EF Core 6.0 vytvoří více než jednu dávku. Například při odstraňování a vkládání řádků do stejné tabulky.

Hodnota SaveChanges

Některé z příkladů zde ukazují, že ukládání výsledků do databáze může být složitá firma. To je místo, kde použití něco jako EF Core skutečně ukazuje jeho hodnotu. EF Core:

  • Batches multiple insert, update, and delete commands together to reduce roundtrips
  • Zjistí, jestli je explicitní transakce potřebná nebo ne
  • Určuje, v jakém pořadí se mají vložit, aktualizovat a odstranit entity, aby nebyla porušena omezení databáze.
  • Zajišťuje, aby se hodnoty generované databází vracely efektivně a šířily zpět do entit.
  • Automaticky nastaví hodnoty cizího klíče pomocí hodnot vygenerovaných pro primární klíče.
  • Zjišťování konfliktů souběžnosti

Kromě toho různé databázové systémy vyžadují pro mnoho z těchto případů různé SQL. Zprostředkovatel databáze EF Core pracuje s EF Core, aby se zajistilo, že se pro každý případ odešlou správné a efektivní příkazy.

Mapování dědičnosti typu TPC (Table-per-beton-type)

EF Core ve výchozím nastavení mapuje hierarchii dědičnosti typů .NET na jednoúčelovou tabulku databáze. Označuje se jako strategie mapování TPH (table-per-hierarchy). EF Core 5.0 zavedla strategii tpT (table-per-type), která podporuje mapování jednotlivých typů .NET na jinou tabulku databáze. EF7 zavádí strategii TPC (table-per-beton-type). TPC také mapuje typy .NET na různé tabulky, ale způsobem, který řeší některé běžné problémy s výkonem strategie TPT.

Tip

Zde uvedený kód pochází z TpcInheritanceSample.cs.

Tip

Ef Team demonstroval a podrobně mluvil o mapování TPC v epizodě standupu komunity dat .NET. Stejně jako u všech epizod Community Standup můžete sledovat epizodu TPC nyní na YouTube.

Schéma databáze TPC

Strategie TPC se podobá strategii TPT s tím rozdílem, že pro každý konkrétní typ v hierarchii se vytvoří jiná tabulka, ale tabulky se nevytvoří pro abstraktní typy– proto se název "table-per-beton-type". Stejně jako u TPT označuje samotná tabulka typ uloženého objektu. Na rozdíl od mapování TPT však každá tabulka obsahuje sloupce pro každou vlastnost konkrétního typu a jeho základní typy. Schémata databáze TPC jsou denormalizovaná.

Zvažte například mapování této hierarchie:

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

Při použití SQL Serveru jsou tabulky vytvořené pro tuto hierarchii následující:

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]));

Všimněte si, že:

  • Neexistují žádné tabulky pro tyto Animal typy Pet , protože jsou abstract v objektovém modelu. Mějte na paměti, že jazyk C# neumožňuje instance abstraktních typů, a proto neexistuje situace, kdy se do databáze uloží instance abstraktního typu.

  • Mapování vlastností v základních typech se opakuje pro každý konkrétní typ. Každá tabulka má Name například sloupec a kočky i psi mají Vet sloupec.

  • Uložením některých dat do této databáze vznikne následující:

Tabulka kočky

Id Name FoodId Veterinář EducationLevel
0 Alice 99ca3e98-b26d-4a0c-d4ae-08da7aca624f Pengelly MBA
2 Mac 99ca3e98-b26d-4a0c-d4ae-08da7aca624f Pengelly Předškolní
8 Baxter 5dc5019e-6f72-454b-d4b0-08da7aca624f Bothell Pet Hospital Bsc

Tabulka Psi

Id Name FoodId Veterinář FavoriteToy
3 Toust 011aaf6f-d588-4fad-d4ac-08da7aca624f Pengelly Pan Veverka

Tabulka FarmAnimals

Id Name FoodId Hodnota Druh
4 Clyde 1d495075-f527-4498-d4af-08da7aca624f 100.00 Equus africanus asinus

Tabulka Lidé

Id Name FoodId FavoriteAnimalId
5 Wendy 5418fd81-7660-432f-d4b1-08da7aca624f 2
6 Arthur 59b495d4-0414-46bf-d4ad-08da7aca624f 1
9 Katie null 8

Všimněte si, že na rozdíl od mapování TPT jsou všechny informace pro jeden objekt obsaženy v jedné tabulce. A na rozdíl od mapování TPH neexistuje žádná kombinace sloupců a řádků v žádné tabulce, ve které model nikdy nepoužívá. Níže uvidíme, jak můžou být tyto charakteristiky pro dotazy a úložiště důležité.

Konfigurace dědičnosti TPC

Při mapování hierarchie ef Core musí být všechny typy v hierarchii dědičnosti explicitně zahrnuty do modelu. Můžete to provést vytvořením DbSet vlastností DbContext pro každý typ:

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>();

Nebo pomocí Entity metody v OnModelCreating:

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

Důležité

To se liší od starší verze chování EF6, kdy se odvozené typy mapovaných základních typů automaticky zjistí, pokud by byly obsaženy ve stejném sestavení.

K mapování hierarchie jako TPH není potřeba nic jiného udělat, protože se jedná o výchozí strategii. Od EF7 je však TPH možné explicitně provést voláním UseTphMappingStrategy základního typu hierarchie:

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

Pokud chcete místo toho použít TPT, změňte tuto možnost na UseTptMappingStrategy:

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

UseTpcMappingStrategy Podobně se používá ke konfiguraci TPC:

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

V každém případě je název tabulky použitý pro každý typ převzat z DbSet názvu vlastnosti ve vaší DbContext, nebo lze nakonfigurovat pomocí ToTable metody tvůrce nebo atributu [Table] .

Výkon dotazů TPC

U dotazů je strategie TPC vylepšením oproti tpT, protože zajišťuje, aby informace pro danou instanci entity byly vždy uloženy v jedné tabulce. To znamená, že strategie TPC může být užitečná, když je mapovaná hierarchie velká a má mnoho konkrétních typů (obvykle list), z nichž každý má velký počet vlastností a kde se ve většině dotazů používá jenom malá podmnožina typů.

Sql vygenerovaný pro tři jednoduché dotazy LINQ lze použít k pozorování, kde TPC funguje dobře ve srovnání s TPH a TPT. Mezi tyto dotazy patří:

  1. Dotaz, který vrací entity všech typů v hierarchii:

    context.Animals.ToList();
    
  2. Dotaz, který vrací entity z podmnožině typů v hierarchii:

    context.Pets.ToList();
    
  3. Dotaz, který vrací pouze entity z jednoho typu typu list v hierarchii:

    context.Cats.ToList();
    

Dotazy TPH

Při použití TPH se všechny tři dotazy dotazují pouze na jednu tabulku, ale s různými filtry pro nediskriminační sloupec:

  1. TPH SQL vracející entity všech typů v 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. TPH SQL vracející entity z podmnožina typů v 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. TPH SQL vracející pouze entity z jednoho typu typu list v hierarchii:

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

Všechny tyto dotazy by měly dobře fungovat, zejména s odpovídajícím indexem databáze v nediskriminačním sloupci.

Dotazy TPT

Při použití TPT vyžadují všechny tyto dotazy spojování více tabulek, protože data pro libovolný konkrétní typ jsou rozdělená mezi mnoho tabulek:

  1. TPT SQL vracející entity všech typů v 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 vracející entity z podmnožina typů v 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 vracející pouze entity z jednoho typu typu list v 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]
    

Poznámka:

EF Core používá "nediskriminační syntézu" k určení tabulky, ze které data pocházejí, a proto správný typ, který se má použít. To funguje, protože funkce LEFT JOIN vrací hodnoty null pro sloupec závislého ID ("dílčí tabulky"), které nejsou správným typem. Takže pro psa bude [d].[Id] nenulová a všechny ostatní (betonové) ID budou null.

Všechny tyto dotazy můžou mít problémy s výkonem způsobené spojeními tabulek. Proto tpT není nikdy dobrou volbou pro výkon dotazů.

Dotazy TPC

TPC zlepšuje hodnotu TPT pro všechny tyto dotazy, protože počet tabulek, které je potřeba dotazovat, je snížen. Kromě toho se výsledky z každé tabulky kombinují pomocí UNION ALL, což může být výrazně rychlejší než spojení tabulky, protože nemusí provádět žádné shody mezi řádky nebo odstraněním duplicit řádků.

  1. TPC SQL vracející entity všech typů v 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 vracející entity z podmnožina typů v 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 vracející pouze entity z jednoho typu typu list v hierarchii:

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

I když je TPC lepší než TPT pro všechny tyto dotazy, jsou dotazy TPH stále lepší při vracení instancí více typů. Toto je jeden z důvodů, proč je TPH výchozí strategií používanou EF Core.

Jak ukazuje SQL pro dotaz č. 3, TPC ve skutečnosti exceluje při dotazování entit jednoho typu list. Dotaz používá jenom jednu tabulku a nepotřebuje žádné filtrování.

Vkládání a aktualizace TPC

TPC také funguje dobře při ukládání nové entity, protože to vyžaduje vložení pouze jednoho řádku do jedné tabulky. To platí i pro TPH. V případě TPT je nutné řádky vložit do mnoha tabulek, které budou fungovat méně dobře.

Totéž platí často pro aktualizace, i když v tomto případě, pokud jsou všechny sloupce aktualizované ve stejné tabulce, i pro TPT, pak rozdíl nemusí být významný.

Důležité informace o prostoru

TpT i TPC můžou používat méně úložiště než TPH, pokud existuje mnoho podtypů s mnoha vlastnostmi, které se často nepoužívají. Důvodem je to, že každý řádek v tabulce TPH musí obsahovat NULL pro každý z těchto nepoužívaných vlastností. V praxi se jedná o zřídka problém, ale při ukládání velkých objemů dat s těmito charakteristikami by mohlo být vhodné zvážit.

Tip

Pokud ho váš databázový systém podporuje (e.g. SQL Server), zvažte použití "řídkých sloupců" pro sloupce TPH, které budou zřídka vyplněny.

Generování klíčů

Zvolená strategie mapování dědičnosti má důsledky pro generování a správu hodnot primárního klíče. Klíče v TPH jsou jednoduché, protože každá instance entity je reprezentována jedním řádkem v jedné tabulce. Je možné použít jakékoli generování hodnot klíče a nejsou potřeba žádná další omezení.

Pro strategii TPT existuje vždy řádek v tabulce namapovaný na základní typ hierarchie. Na tomto řádku lze použít jakékoli generování klíčů a klíče pro jiné tabulky jsou propojeny s touto tabulkou pomocí omezení cizího klíče.

Věci jsou pro TPC trochu složitější. Nejprve je důležité pochopit, že EF Core vyžaduje, aby všechny entity v hierarchii měly jedinečnou hodnotu klíče, a to i v případě, že entity mají různé typy. Takže při použití našeho ukázkového modelu nemůže pes mít stejnou hodnotu klíče ID jako kočka. Zadruhé, na rozdíl od TPT neexistuje žádná běžná tabulka, která může fungovat jako jediné místo, kde jsou hodnoty klíčů živé a lze je vygenerovat. To znamená, že nelze použít jednoduchý Identity sloupec.

U databází, které podporují sekvence, je možné hodnoty klíčů vygenerovat pomocí jediné sekvence odkazované ve výchozím omezení pro každou tabulku. Toto je strategie použitá v tabulkách TPC uvedených výše, kde každá tabulka obsahuje následující:

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

AnimalSequence je sekvence databáze vytvořená ef Core. Tato strategie se ve výchozím nastavení používá pro hierarchie TPC při použití poskytovatele databáze EF Core pro SQL Server. Poskytovatelé databází pro jiné databáze, které podporují sekvence, by měly mít podobné výchozí nastavení. Další strategie generování klíčů, které používají sekvence, jako jsou vzory Hi-Lo, mohou být také použity s TPC.

I když standardní sloupce identity nebudou fungovat s TPC, je možné použít sloupce Identity, pokud je každá tabulka nakonfigurovaná s odpovídajícím počátečním a přírůstkem tak, aby hodnoty vygenerované pro každou tabulku nikdy nebyly v konfliktu. Příklad:

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 nepodporuje sekvence ani počáteční nebo přírůstkové hodnoty identity, a proto generování celočíselných hodnot klíče není podporováno při použití SQLite se strategií TPC. Generování na straně klienta nebo globálně jedinečné klíče – například klíče GUID – jsou podporovány v jakékoli databázi, včetně SQLite.

Omezení cizího klíče

Strategie mapování TPC vytvoří denormalizované schéma SQL – to je jeden z důvodů, proč jsou některé databáze purists proti. Představte si například sloupec FavoriteAnimalIdcizího klíče . Hodnota v tomto sloupci musí odpovídat hodnotě primárního klíče některého zvířete. To lze vynutit v databázi s jednoduchým omezením FK při použití TPH nebo TPT. Příklad:

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

Ale při použití TPC, primární klíč pro zvíře je uložen v tabulce pro konkrétní typ tohoto zvířete. Primární klíč kočky je například uložen ve Cats.Id sloupci, zatímco primární klíč psa je uložen ve Dogs.Id sloupci atd. To znamená, že pro tuto relaci nelze vytvořit omezení FK.

V praxi to není problém, pokud se aplikace nepokoušá vložit neplatná data. Pokud jsou například všechna data vložena pomocí EF Core a používá navigace k propojení entit, je zaručeno, že sloupec FK bude obsahovat platnou hodnotu PK vždy.

Shrnutí a pokyny

V souhrnu je TPC dobrou strategií mapování, která se má použít, když se váš kód bude dotazovat na entity jednoho typu typu list. Důvodem je, že požadavky na úložiště jsou menší a neexistuje žádný nediskriminační sloupec, který by mohl potřebovat index. Vkládání a aktualizace jsou také efektivní.

To znamená, že TPH je obvykle v pořádku pro většinu aplikací a je dobrým výchozím nastavením pro širokou škálu scénářů, takže pokud ho nepotřebujete, nepřidávejte složitost TPC. Konkrétně platí, že pokud se váš kód bude dotazovat na entity mnoha typů, jako je zápis dotazů na základní typ, pak se naklonit k TPH přes TPC.

TpT používejte jenom v případě, že je to omezené externími faktory.

Vlastní šablony zpětné analýzy

Nově můžete přizpůsobit vygenerovaný kód při zpětné analýze modelu EF z databáze. Začněte přidáním výchozích šablon do projektu:

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

Šablony je pak možné přizpůsobit a automaticky je bude používat dotnet ef dbcontext scaffold Scaffold-DbContext.

Další podrobnosti najdete v tématu Vlastní šablony zpětné analýzy.

Tip

Tým EF demonstroval a podrobně mluvil o šablonách zpětné analýzy v epizodě standupu komunity dat .NET. Stejně jako u všech epizod komunitních standupů můžete sledovat epizodu šablon T4 nyní na YouTube.

Konvence vytváření modelů

EF Core používá k popisu mapování typů entit aplikace na podkladovou databázi metadata "model". Tento model je sestaven pomocí sady přibližně 60 "konvencí". Model vytvořený konvencí je pak možné přizpůsobit pomocí atributů mapování (neboli "datových poznámek") a/nebo voláníDbModelBuilder rozhraní API v OnModelCreating.

Od EF7 teď můžou aplikace odebrat nebo nahradit některou z těchto konvencí a také přidávat nové konvence. Konvence vytváření modelů představují účinný způsob, jak řídit konfiguraci modelu, ale může být složité a obtížné se dostat doprava. V mnoha případech je možné použít stávající konfiguraci modelu před konvencí k snadnému určení společné konfigurace vlastností a typů.

Změny konvencí používaných metodou DbContext jsou provedeny přepsáním DbContext.ConfigureConventions metody. Příklad:

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

Tip

Pokud chcete najít všechny integrované konvence vytváření modelů, vyhledejte všechny třídy, které implementují IConvention rozhraní.

Tip

Zde uvedený kód pochází z ModelBuildingConventionsSample.cs.

Odebrání existující konvence

Někdy nemusí být jedna z předdefinovaných konvencí vhodná pro vaši aplikaci, v takovém případě ji můžete odebrat.

Příklad: Nevytvávejte indexy pro sloupce cizího klíče

Obvykle dává smysl vytvářet indexy pro sloupce cizího klíče (FK), a proto existuje předdefinovaná konvence pro toto: ForeignKeyIndexConvention. Při pohledu na zobrazení ladění modelu pro Post typ entity s relacemi Blog a Authorvidíme, že dva indexy jsou vytvořené – jeden pro FK a druhý pro BlogId AuthorId 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

Indexy však mají režijní náklady a jak je zde uvedeno, nemusí být vždy vhodné je vytvořit pro všechny sloupce FK. Abyste toho dosáhli, ForeignKeyIndexConvention můžete ho při sestavování modelu odebrat:

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

Když se teď podíváme na zobrazení ladění modelu Post , vidíme, že se nevytvořily indexy ve FK:

  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

V případě potřeby je možné indexy explicitně vytvořit pro sloupce cizího klíče, a to buď pomocí:IndexAttribute

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

Nebo s konfigurací v OnModelCreating:

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

Když se znovu podíváte na Post typ entity, bude teď obsahovat BlogId index, ale ne AuthorId index:

  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

Tip

Pokud váš model pro konfiguraci nepoužívá atributy mapování (neboli datové poznámky), všechny konvence končící AttributeConvention na model je možné bezpečně odebrat, aby se urychlil vytváření modelu.

Přidání nové konvence

Odebrání existujících konvencí je začátek, ale co přidání úplně nových konvencí vytváření modelů? EF7 to také podporuje!

Příklad: Omezení délky nediskriminačních vlastností

Strategie mapování dědičnosti tabulek na hierarchii vyžaduje diskriminující sloupec k určení typu, který je reprezentován v jakémkoli daném řádku. Ef ve výchozím nastavení používá pro diskriminátor nevázaný řetězec sloupec, který zajistí, že bude fungovat pro jakoukoliv nediskriminační délku. Omezení maximální délky nediskriminačních řetězců ale může usnadnit ukládání a dotazy. Pojďme vytvořit novou konvenci, která to provede.

Konvence vytváření modelů EF Core se aktivují na základě změn provedených v modelu při vytváření. Díky tomu bude model aktuální, protože se vytvoří explicitní konfigurace, použijí se atributy mapování a spustí se další konvence. Aby se této konvence účastnila, implementuje každá konvence jedno nebo více rozhraní, která určují, kdy se konvence aktivuje. Například konvence, která implementuje IEntityTypeAddedConvention , se aktivuje při každém přidání nového typu entity do modelu. Podobně platí, že konvence, která implementuje obojí IForeignKeyAddedConvention a IKeyAddedConvention bude aktivována při každém přidání klíče nebo cizího klíče do modelu.

Znalost rozhraní, která se mají implementovat, může být složité, protože konfigurace modelu v jednom okamžiku může být změněna nebo odebrána v pozdějším okamžiku. Například klíč může být vytvořen konvencí, ale později nahrazen, když je explicitně nakonfigurován jiný klíč.

Pojďme to trochu konkrétněji provést prvním pokusem o implementaci nediskriminační konvence délky:

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

Tato konvence implementuje IEntityTypeBaseTypeChangedConvention, což znamená, že se aktivuje při každé změně mapované hierarchie dědičnosti pro typ entity. Konvence pak vyhledá a nakonfiguruje diskriminující vlastnost řetězce pro hierarchii.

Tato konvence se pak používá voláním Add ConfigureConventions:

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

Tip

Místo přímého Add přidání instance konvence metoda přijímá objekt pro vytváření instancí konvence. To umožňuje konvenci používat závislosti od interního poskytovatele služeb EF Core. Vzhledem k tomu, že tato konvence neobsahuje žádné závislosti, je parametr poskytovatele služeb pojmenován _, což znamená, že se nikdy nepoužívá.

Sestavení modelu a zobrazení Post typu entity ukazuje, že tato diskriminující vlastnost je nyní nakonfigurována na maximální délku 24:

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

Co se ale stane, když teď explicitně nakonfigurujeme jinou nediskriminační vlastnost? Příklad:

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

Při pohledu na ladění modelu zjistíme, že diskriminující délka už není nakonfigurovaná.

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

Je to proto, že při přidání vlastního diskriminace byla později odstraněna diskriminující vlastnost, kterou jsme nakonfigurovali v naší úmluvě. Můžeme se pokusit tento problém vyřešit implementací jiného rozhraní na naší konvenci, abychom reagovali na nediskriminační změny, ale zjištění, které rozhraní k implementaci není snadné.

Naštěstí existuje jiný způsob, jak k tomu přistupovat, což usnadňuje práci. Hodně času nezáleží na tom, jak model vypadá při jeho sestavení, pokud je konečný model správný. Kromě toho konfigurace, kterou chceme použít, často nemusí aktivovat další konvence pro reakci. Proto naše konvence může implementovat IModelFinalizingConvention. Konvence finalizace modelu se spouští po dokončení všech ostatních sestavení modelu a mají tak přístup k poslednímu stavu modelu. Konvence finalizace modelu se obvykle iteruje nad celým modelem, který konfiguruje prvky modelu, jak pokračuje. V tomto případě tedy v modelu najdeme každou diskriminaci a nakonfigurujeme ji:

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 vytvoření modelu pomocí této nové konvence zjistíme, že diskriminující délka je nyní správně nakonfigurovaná, i když byla upravena:

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

Jen pro zábavu pojďme ještě o krok dál a nakonfigurujeme maximální délku tak, aby byla délka nejdelší diskriminující hodnoty.

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

Nyní je diskriminující maximální délka sloupce 8, což je délka "Vybrané", nejdelší diskriminující hodnota, která se používá.

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

Tip

Možná vás zajímá, jestli by konvence měla vytvořit také index pro nediskriminační sloupec. Na GitHubu je k dispozici diskuze. Stručná odpověď je, že někdy může být index užitečný, ale většina času pravděpodobně nebude. Proto je nejlepší vytvořit vhodné indexy podle potřeby, místo abyste měli konvenci, aby to bylo vždy. Pokud ale s tím nesouhlasíte, můžete výše uvedenou konvenci snadno upravit a vytvořit také index.

Příklad: Výchozí délka všech vlastností řetězce

Podívejme se na další příklad, ve kterém je možné použít konvenci finalizace – tentokrát nastavíme výchozí maximální délku pro libovolnou vlastnost řetězce, jak se požaduje na GitHubu. Konvence vypadá poměrně podobně jako v předchozím příkladu:

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

Tato konvence je docela jednoduchá. Najde každou vlastnost řetězce v modelu a nastaví její maximální délku na 512. Při pohledu na vlastnosti Postladicí zobrazení vidíme, že všechny vlastnosti řetězce teď mají maximální délku 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 vlastnost by měla pravděpodobně povolit více než 512 znaků, nebo všechny naše příspěvky budou docela krátké! To lze provést beze změny naší konvence explicitní konfigurací maximální délky pouze pro tuto vlastnost, a to buď pomocí atributu mapování:

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

Nebo s kódem v OnModelCreating:

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

Všechny vlastnosti teď mají maximální délku 512, s výjimkou Content toho, který byl explicitně nakonfigurován s hodnotou 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)

Proč tedy naše konvence nepřepsala explicitně nakonfigurovanou maximální délku? Odpovědí je, že EF Core sleduje, jak byla provedena každá část konfigurace. Toto je reprezentováno výčtem ConfigurationSource . Mezi různé druhy konfigurace patří:

  • Explicit: Element modelu byl explicitně nakonfigurován v OnModelCreating
  • DataAnnotation: Element modelu byl nakonfigurován pomocí atributu mapování (neboli datové poznámky) typu CLR.
  • Convention: Prvek modelu byl nakonfigurován konvencí vytváření modelu.

Konvence nikdy nepřepíší konfiguraci označenou jako DataAnnotation nebo Explicit. Toho lze dosáhnout pomocí "tvůrce konvence", IConventionPropertyBuildernapříklad , který je získán z Builder vlastnosti. Příklad:

property.Builder.HasMaxLength(512);

Volání HasMaxLength tvůrce konvencí nastaví maximální délku pouze v případě, že ještě nebyl nakonfigurován atributem mapování nebo v OnModelCreating.

Metody tvůrce, jako je tento, mají také druhý parametr: fromDataAnnotation. Tuto možnost nastavte, true pokud konvence provádí konfiguraci jménem atributu mapování. Příklad:

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

Tím se nastaví ConfigurationSource hodnota DataAnnotation, což znamená, že hodnotu lze nyní přepsat explicitním mapováním , OnModelCreatingale ne konvencí atributů bez mapování.

A nakonec, než tento příklad opustíme, co se stane, když použijeme obojí MaxStringLengthConvention i DiscriminatorLengthConvention3 ve stejnou dobu? Odpověď spočívá v tom, jaké pořadí se přidají, protože konvence finalizace modelu se spouštějí v pořadí, v jakém se přidají. Pokud MaxStringLengthConvention tedy bude přidána jako poslední, spustí se naposledy a nastaví maximální délku nediskriminační vlastnosti na 512. Proto je v tomto případě lepší přidat DiscriminatorLengthConvention3 poslední, aby mohl přepsat výchozí maximální délku pouze diskriminačních vlastností a ponechat všechny ostatní vlastnosti řetězce jako 512.

Nahrazení existující konvence

Někdy místo toho, abychom úplně odstranili existující konvenci, chceme ji nahradit konvencí, která v podstatě dělá totéž, ale se změněným chováním. To je užitečné, protože stávající konvence už implementuje rozhraní, která potřebuje, aby se aktivovala odpovídajícím způsobem.

Příklad: Mapování vlastností opt-in

EF Core mapuje všechny veřejné vlastnosti pro čtení i zápis podle konvence. To nemusí být vhodné pro způsob, jakým jsou definovány typy entit. Abychom to změnili, můžeme nahradit PropertyDiscoveryConvention vlastní implementací, která nemapuje žádnou vlastnost, pokud není explicitně namapována OnModelCreating nebo označena novým atributem s názvem Persist:

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

Tady je nová konvence:

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

Tip

Při nahrazení předdefinované konvence by nová implementace konvence měla dědit z existující třídy konvence. Všimněte si, že některé konvence mají relační implementace nebo implementace specifické pro zprostředkovatele, v takovém případě by nová implementace konvence měla dědit z nejvýraznější existující třídy konvence pro poskytovatele databáze, který se používá.

Konvence se pak zaregistruje pomocí Replace metody v ConfigureConventions:

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

Tip

Jedná se o případ, kdy existující konvence obsahuje závislosti reprezentované objektem ProviderConventionSetBuilderDependencies závislosti. Ty jsou získány od interního poskytovatele služeb pomocí GetRequiredService a předány konstruktoru konvence.

Tato konvence funguje tak, že z daného typu entity získá všechny čitelné vlastnosti a pole. Pokud je člen atributem [Persist], pak je mapován voláním:

entityTypeBuilder.Property(memberInfo);

Pokud je naopak člen vlastností, která by jinak byla mapována, je vyloučena z modelu pomocí:

entityTypeBuilder.Ignore(propertyInfo.Name);

Všimněte si, že tato konvence umožňuje mapování polí (kromě vlastností), pokud jsou označena značkou [Persist]. To znamená, že privátní pole můžeme použít jako skryté klíče v modelu.

Představte si například následující typy entit:

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 vytvořený z těchto typů entit je:

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

Všimněte si, že normálně by bylo mapováno, ale protože není označeno [Persist] (pravděpodobně proto, IsClean že čistota není trvalou vlastností prádelny), je nyní považována za nemapovanou vlastnost.

Tip

Tuto konvenci nebylo možné implementovat jako konvenci finalizace modelu, protože mapování vlastnosti spouští mnoho dalších konvencí, které se mají spustit, aby se mapovaná vlastnost dále nakonfigurovala.

Mapování uložených procedur

EF Core ve výchozím nastavení generuje příkazy pro vložení, aktualizaci a odstranění, které pracují přímo s tabulkami nebo aktualizovatelnými zobrazeními. EF7 zavádí podporu pro mapování těchto příkazů na uložené procedury.

Tip

EF Core vždy podporuje dotazování prostřednictvím uložených procedur. Nová podpora ef7 je explicitně o používání uložených procedur pro vložení, aktualizace a odstranění.

Důležité

Podpora mapování uložených procedur neznamená, že se doporučují uložené procedury.

Uložené procedury jsou mapovány pomocí OnModelCreating , InsertUsingStoredProcedureUpdateUsingStoredProcedurea DeleteUsingStoredProcedure. Pokud chcete například mapovat uložené procedury pro Person typ entity:

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

Tato konfigurace se při použití SQL Serveru mapuje na následující uložené procedury:

Pro vložení

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

Aktualizace

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

Odstranění

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

Tip

Uložené procedury nemusí být použity pro každý typ modelu ani pro všechny operace s daným typem. Pokud je například zadán pouze DeleteUsingStoredProcedure pro daný typ, EF Core vygeneruje SQL jako normální pro operace vložení a aktualizace a použije pouze uloženou proceduru pro odstranění.

První argument předaný každé metodě je název uložené procedury. To je možné vynechat, v takovém případě EF Core použije název tabulky, který je připojený s "_Insert", "_Update" nebo "_Delete". V příkladu výše je tedy možné odebrat názvy uložených procedur beze změny funkčnosti, protože tabulka se nazývá Lidé.

Druhým argumentem je tvůrce, který slouží ke konfiguraci vstupu a výstupu uložené procedury, včetně parametrů, návratových hodnot a výsledných sloupců.

Parametry

Parametry musí být přidány do tvůrce ve stejném pořadí, v jakém se zobrazují v definici uložené procedury.

Poznámka:

Parametry lze pojmenovat, ale EF Core vždy volá uložené procedury pomocí pozičních argumentů, nikoli pojmenovaných argumentů. Hlasujte pro povolení konfigurace mapování sproc pro použití názvů parametrů pro vyvolání , pokud volání podle názvu je něco, co vás zajímá.

První argument pro každou metodu tvůrce parametrů určuje vlastnost v modelu, ke kterému je parametr vázán. Může to být výraz lambda:

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

Nebo řetězec, který je zvláště užitečný při mapování stínových vlastností:

storedProcedureBuilder.HasParameter("Name");

Parametry jsou ve výchozím nastavení nakonfigurované pro "vstup". Pomocí vnořeného tvůrce je možné nakonfigurovat parametry "Output" nebo "input/output". Příklad:

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

Existují tři různé metody tvůrce pro různé varianty parametrů:

  • HasParameter určuje normální parametr vázaný na aktuální hodnotu dané vlastnosti.
  • HasOriginalValueParameter určuje parametr vázaný na původní hodnotu dané vlastnosti. Původní hodnota je hodnota, kterou vlastnost měla při dotazech z databáze, pokud je známá. Pokud tato hodnota není známá, použije se místo toho aktuální hodnota. Parametry původní hodnoty jsou užitečné pro tokeny souběžnosti.
  • HasRowsAffectedParameter určuje parametr použitý k vrácení počtu řádků ovlivněných uloženou procedurou.

Tip

Parametry původní hodnoty musí být použity pro hodnoty klíče v uložených procedurách update a delete. Tím se zajistí aktualizace správného řádku v budoucích verzích EF Core, které podporují proměnlivé hodnoty klíčů.

Vrácení hodnot

EF Core podporuje tři mechanismy pro vrácení hodnot z uložených procedur:

  • Výstupní parametry, jak je znázorněno výše.
  • Výsledné sloupce, které jsou zadané pomocí HasResultColumn metody tvůrce.
  • Vrácená hodnota, která je omezena na vrácení počtu ovlivněných řádků a je určena pomocí HasRowsAffectedReturnValue metody tvůrce.

Hodnoty vrácené z uložených procedur se často používají pro vygenerované, výchozí nebo vypočítané hodnoty, například z Identity klíče nebo počítaného sloupce. Například následující konfigurace určuje čtyři sloupce výsledků:

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

Ty se používají k vrácení:

  • Vygenerovaná hodnota klíče pro Id vlastnost.
  • Výchozí hodnota vygenerovaná databází pro FirstRecordedOn vlastnost.
  • Vypočítaná hodnota vygenerovaná databází pro RetrievedOn vlastnost.
  • Automaticky vygenerovaný rowversion token souběžnosti pro RowVersion vlastnost.

Tato konfigurace se při použití SQL Serveru mapuje na následující uloženou proceduru:

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

Optimistická metoda souběžného zpracování

Optimistická souběžnost funguje stejně s uloženými procedurami jako bez ní. Uložená procedura by měla:

  • Pomocí tokenu souběžnosti v WHERE klauzuli se ujistěte, že se řádek aktualizuje pouze v případě, že má platný token. Hodnota použitá pro token souběžnosti je obvykle, ale nemusí být původní hodnota vlastnosti tokenu souběžnosti.
  • Vrátí počet ovlivněných řádků, aby ef Core mohl porovnat tento počet s očekávaným počtem ovlivněných řádků a vyvolat DbUpdateConcurrencyException chybu, pokud se hodnoty neshodují.

Například následující uložená rowversion procedura SQL Serveru používá token automatické souběžnosti:

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

Toto je nakonfigurované v EF Core pomocí:

.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();
    });

Všimněte si, že:

  • Použije se původní hodnota tokenu RowVersion souběžnosti.
  • Uložená procedura používá WHERE klauzuli k zajištění, že se řádek aktualizuje pouze v případě, RowVersion že původní hodnota odpovídá.
  • Nová vygenerovaná hodnota je RowVersion vložena do dočasné tabulky.
  • Vrátí se počet ovlivněných řádků (@@ROWCOUNT) a vygenerovaná RowVersion hodnota.

Mapování hierarchií dědičnosti na uložené procedury

EF Core vyžaduje, aby uložené procedury dodržovaly rozložení tabulky pro typy v hierarchii. To znamená, že:

  • Hierarchie mapovaná pomocí TPH musí obsahovat jednu uloženou proceduru vložení, aktualizaci nebo odstranění, která cílí na jednu mapovanou tabulku. Uložené procedury vložení a aktualizace musí mít parametr pro diskriminující hodnotu.
  • Hierarchie mapovaná pomocí TPT musí obsahovat uloženou proceduru vložení, aktualizace a/nebo odstranění pro každý typ, včetně abstraktních typů. EF Core provede podle potřeby několik volání pro aktualizaci, vložení a odstranění řádků ve všech tabulkách.
  • Hierarchie namapovaná pomocí TPC musí obsahovat uloženou proceduru vložení, aktualizace a/nebo odstranění pro každý konkrétní typ, ale ne abstraktní typy.

Poznámka:

Pokud používáte jednu uloženou proceduru na konkrétní typ bez ohledu na strategii mapování, máte zájem o podporu, hlasujte o podporu pomocí jednoho sproc na konkrétní typ bez ohledu na strategii mapování dědičnosti.

Mapování vlastněných typů na uložené procedury

Konfigurace uložených procedur pro vlastněné typy se provádí v tvůrci vnořených vlastněných typů. Příklad:

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

Poznámka:

Aktuálně uložené procedury pro vložení, aktualizaci a odstranění podporují pouze vlastněné typy musí být mapovány na samostatné tabulky. To znamená, že vlastněný typ nemůže být reprezentován sloupci v tabulce vlastníka. Hlasujte pro přidání "tabulky" rozdělení podpory na mapování CUD sproc, pokud se jedná o omezení, které byste chtěli vidět odebrané.

Mapování entit spojení M:N na uložené procedury

Konfiguraci uložených procedur spojení M:N je možné provádět jako součást konfigurace M:N. Příklad:

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

Nové a vylepšené průsečíky a události

Průsečíky EF Core umožňují zachytit, upravit a/nebo potlačení operací EF Core. EF Core obsahuje také tradiční události a protokolování .NET.

EF7 obsahuje následující vylepšení průsečíků:

Kromě toho EF7 obsahuje nové tradiční události .NET pro:

Následující části ukazují některé příklady použití těchto nových funkcí zachycení.

Jednoduché akce při vytváření entit

Tip

Zde uvedený kód pochází z SimpleMaterializationSample.cs.

Nový IMaterializationInterceptor podporuje zachycení před a po vytvoření instance entity a před a po vlastnosti této instance jsou inicializovány. Průsečík může v každém bodě změnit nebo nahradit instanci entity. To dovoluje:

  • Nastavení nenamapovaných vlastností nebo volání metod potřebných k ověření, vypočítaným hodnotám nebo příznakům
  • Vytváření instancí pomocí továrny
  • Vytvoření jiné instance entity než EF by normálně vytvořilo, například instanci z mezipaměti nebo typu proxy serveru.
  • Vkládání služeb do instance entity

Představte si například, že chceme sledovat čas, kdy byla entita načtena z databáze, aby se mohla zobrazit uživateli, který data upravuje. Abychom toho dosáhli, nejprve definujeme rozhraní:

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

Použití rozhraní je běžné s průsečíky, protože umožňuje, aby stejný průsečík fungoval s mnoha různými typy entit. Příklad:

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

Všimněte si, že [NotMapped] atribut slouží k označení, že tato vlastnost se používá pouze při práci s entitou a neměla by být zachována v databázi.

Zachycovač pak musí implementovat příslušnou metodu z IMaterializationInterceptor a nastavit čas načtení:

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

        return instance;
    }
}

Instance tohoto průsečíku je registrována při konfiguraci 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");
}

Tip

Tento průsečík je bezstavový, což je běžné, takže se vytvoří a sdílí jedna instance mezi všemi DbContext instancemi.

Když se teď dotazuje Customer z databáze, Retrieved vlastnost se nastaví automaticky. Příklad:

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

Vytvoří výstup:

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

Vkládání služeb do entit

Tip

Zde uvedený kód pochází z InjectLoggerSample.cs.

EF Core již má integrovanou podporu pro vkládání některých speciálních služeb do kontextových instancí; Podívejte se například na opožděné načítání bez proxy serverů, které funguje vložením ILazyLoader služby.

IMaterializationInterceptor Tuto akci lze použít ke zobecnění jakékoli služby. Následující příklad ukazuje, jak vložit do ILogger entit, aby mohli provádět vlastní protokolování.

Poznámka:

Vkládání služeb do entit páruje tyto typy entit s vloženými službami, které někteří lidé považují za anti-vzor.

Stejně jako předtím se rozhraní používá k definování toho, co lze provést.

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

A typy entit, které budou protokolovat, musí implementovat toto rozhraní. Příklad:

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

Tentokrát musí průsečík implementovat IMaterializationInterceptor.InitializedInstance, který se volá po vytvoření každé instance entity a její hodnoty vlastností byly inicializovány. Průsečík získá ILogger z kontextu a inicializuje IHasLogger.Logger ho:

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

Tentokrát se pro každou DbContext instanci použije nová instance průsečíku, protože ILogger získané instance se můžou změnit DbContext na instanci a je ILogger uložena v mezipaměti v průsečíku:

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

Při každé Customer.PhoneNumber změně se tato změna zaprotokoluje do protokolu aplikace. Příklad:

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

Zachycení stromu výrazů LINQ

Tip

Zde uvedený kód pochází z QueryInterceptionSample.cs.

EF Core využívá dotazy .NET LINQ. To obvykle zahrnuje použití kompilátoru C#, VB nebo F# k sestavení stromu výrazů, který pak EF Core přeloží do příslušného SQL. Představte si například metodu, která vrací stránku zákazníků:

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

Tip

Tento dotaz používá metodu EF.Property k určení vlastnosti, podle které se má seřadit. To aplikaci umožňuje dynamicky předávat název vlastnosti, což umožňuje řazení podle libovolné vlastnosti typu entity. Mějte na paměti, že řazení podle neindexovaných sloupců může být pomalé.

To bude fungovat správně, pokud vlastnost použitá pro řazení vždy vrátí stabilní řazení. Ale to nemusí být vždy případ. Například výše uvedený dotaz LINQ vygeneruje při řazení Customer.Citypomocí příkazu SQLite následující:

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

Pokud existuje více zákazníků se stejným Citydotazem, pořadí tohoto dotazu není stabilní. To může vést k chybějícím nebo duplicitním výsledkům, protože stránky uživatelů procházejí daty.

Běžným způsobem, jak tento problém vyřešit, je provést sekundární řazení podle primárního klíče. Místo ručního přidání do každého dotazu však EF7 umožňuje zachycování stromu výrazů dotazu, kde je možné sekundární řazení přidávat dynamicky. Abychom to usnadnili, znovu použijeme rozhraní, tentokrát pro libovolnou entitu s celočíselnou primárním klíčem:

public interface IHasIntKey
{
    int Id { get; }
}

Toto rozhraní implementují typy entit, které jsou zajímavé:

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

Pak potřebujeme průsečík, který 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 asi vypadá docela komplikovaně- a je to! Práce se stromy výrazů není obvykle snadná. Podívejme se, co se děje:

  • V zásadě, průsečík zapouzdřuje ExpressionVisitor. Návštěvník přepíše VisitMethodCall, který bude volána při každém volání metody ve stromu výrazů dotazu.

  • Návštěvník zkontroluje, jestli se jedná o volání OrderBy metody, která nás zajímá.

  • Pokud ano, návštěvník dále zkontroluje, jestli volání obecné metody je typu, který implementuje naše IHasIntKey rozhraní.

  • V tuto chvíli víme, že volání metody je ve formuláři OrderBy(e => ...). Z tohoto volání extrahujeme výraz lambda a získáme parametr použitý v tomto výrazu - to znamená .e

  • Teď vytvoříme novou MethodCallExpression metodu Expression.Call tvůrce. V tomto případě je volána ThenBy(e => e.Id)metoda . Sestavíme to pomocí parametru extrahovaného výše a vlastnosti přístup k Id vlastnosti IHasIntKey rozhraní.

  • Vstup do tohoto volání je původní OrderBy(e => ...), a proto konečný výsledek je výraz pro OrderBy(e => ...).ThenBy(e => e.Id).

  • Tento upravený výraz se vrátí od návštěvníka, což znamená, že dotaz LINQ byl nyní odpovídajícím způsobem upraven tak, aby zahrnoval ThenBy volání.

  • EF Core pokračuje a zkompiluje tento výraz dotazu do příslušného SQL pro použitou databázi.

Tento průsečík je zaregistrovaný stejným způsobem jako v prvním příkladu. Spuštění GetPageOfCustomers teď vygeneruje následující 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

To nyní vždy vytvoří stabilní objednávání, i když existuje více zákazníků se stejným City.

Uf! To je spousta kódu, který provede jednoduchou změnu dotazu. A ještě horší to nemusí fungovat ani pro všechny dotazy. Je velmi obtížné napsat návštěvníka výrazu, který rozpozná všechny obrazce dotazu, které by měl, a žádný z nich by neměl. Pokud se například řazení provádí v poddotazu, pravděpodobně to nebude fungovat.

To nám přináší kritický bod týkající se průsečíků – vždy se zeptejte, jestli existuje jednodušší způsob, jak dělat to, co chcete. Průsečíky jsou mocné, ale snadno se něco pokazí. Jsou, jak říká, snadný způsob, jak se zastřelit v noze.

Představte si například, že bychom místo toho změnili metodu GetPageOfCustomers takto:

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

V takovém případě ThenBy se do dotazu jednoduše přidá. Ano, může být potřeba provádět zvlášť pro každý dotaz, ale je to jednoduché, snadno pochopitelné a bude vždy fungovat.

Optimistické zachycení souběžnosti

Tip

Zde uvedený kód pochází z OptimisticConcurrencyInterceptionSample.cs.

EF Core podporuje optimistickou souběžnost tím, že kontroluje, jestli počet řádků skutečně ovlivněných aktualizací nebo odstranění je stejný jako počet řádků, které se mají ovlivnit. To je často spojeno s tokenem souběžnosti; to znamená, že hodnota sloupce, která se bude shodovat pouze s očekávanou hodnotou, pokud se řádek od načtení očekávané hodnoty neaktualizoval.

EF signalizuje porušení optimistické souběžnosti vyvoláním DbUpdateConcurrencyExceptionfunkce . V EF7 ISaveChangesInterceptor má nové metody ThrowingConcurrencyException , ThrowingConcurrencyExceptionAsync které se volají před vyvolání DbUpdateConcurrencyException . Tyto průsečíkové body umožňují potlačovat výjimku, což může být spojeno s asynchronními změnami databáze, aby se vyřešilo narušení.

Pokud se například dva požadavky pokusí odstranit stejnou entitu téměř ve stejnou dobu, může druhé odstranění selhat, protože řádek v databázi již neexistuje. To může být v pořádku – konečným výsledkem je, že entita byla přesto odstraněna. Následující průsečík ukazuje, jak to lze provést:

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

Existuje několik věcí, které stojí za zmínku o tomto průsečíku:

  • Implementují se synchronní i asynchronní metody průsečíku. To je důležité, pokud aplikace může volat nebo SaveChanges SaveChangesAsync. Pokud je ale veškerý kód aplikace asynchronní, stačí ho implementovat.ThrowingConcurrencyExceptionAsync Podobně platí, že pokud aplikace nikdy nepoužívá asynchronní databázové metody, je potřeba implementovat pouze ThrowingConcurrencyException . To platí obecně pro všechny průsečíky se synchronizačními a asynchronními metodami. (Může být vhodné implementovat metodu, která se vaší aplikaci nepoužívá, pouze v případě, že se nějaký synchronizovaný/asynchronní kód plíží.)
  • Průsečík má přístup k EntityEntry objektům pro uložené entity. V tomto případě se používá ke kontrole, jestli u operace odstranění dochází nebo nedochází k porušení souběžnosti.
  • Pokud aplikace používá zprostředkovatele relační databáze, ConcurrencyExceptionEventData může být objekt přetypován na RelationalConcurrencyExceptionEventData objekt. Poskytuje další relační informace o prováděné databázové operaci. V tomto případě se text relačního příkazu vytiskne do konzoly.
  • InterceptionResult.Suppress() Vrácení informuje EF Core, aby potlačí akci, o které se v tomto případě jedná, vyvolání DbUpdateConcurrencyException. Tato schopnost měnit chování EF Core, nikoli jen sledovat, co EF Core dělá, je jednou z nejvýkonnějších funkcí průsečíků.

Opožděná inicializace připojovací řetězec

Tip

Zde uvedený kód pochází z LazyConnectionStringSample.cs.

Připojovací řetězce jsou často statické prostředky načtené z konfiguračního souboru. Tyto údaje lze snadno předat UseSqlServer nebo podobné při konfiguraci DbContext. Někdy se ale připojovací řetězec může změnit pro každou instanci kontextu. Každý tenant v systému s více tenanty může mít například jiný připojovací řetězec.

EF7 usnadňuje zpracování dynamických připojení a připojovací řetězec prostřednictvím vylepšení IDbConnectionInterceptorrozhraní . Začíná to schopností konfigurovat DbContext bez připojovací řetězec. Příklad:

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

Jednu z IDbConnectionInterceptor metod je pak možné implementovat ke konfiguraci připojení před jeho použití. ConnectionOpeningAsyncje dobrou volbou, protože může provést asynchronní operaci pro získání připojovací řetězec, vyhledání přístupového tokenu atd. Představte si například službu s oborem aktuálního požadavku, který rozumí aktuálnímu tenantovi:

services.AddScoped<ITenantConnectionStringFactory, TestTenantConnectionStringFactory>();

Upozorňující

Provedení asynchronního vyhledávání pro připojovací řetězec, přístupový token nebo podobné pokaždé, když je potřeba, může být velmi pomalé. Zvažte ukládání těchto věcí do mezipaměti a pravidelně aktualizujte pouze řetězec nebo token uložený v mezipaměti. Například přístupové tokeny se dají často používat po dlouhou dobu před aktualizací.

To lze vložit do každé DbContext instance pomocí injektáže konstruktoru:

public class CustomerContext : DbContext
{
    private readonly ITenantConnectionStringFactory _connectionStringFactory;

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

    // ...
}

Tato služba se pak použije při vytváření implementace zachytávání pro kontext:

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

Zachycovač používá tuto službu k získání připojovací řetězec asynchronně a při prvním použití připojení ho nastavte:

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

Poznámka:

Připojovací řetězec se získá pouze při prvním použití připojení. Potom se připojovací řetězec uložené na zařízení DbConnection použije bez vyhledání nového připojovací řetězec.

Tip

Tento průsečík přepíše metodu, která není asynchronníConnectionOpening, protože služba pro získání připojovací řetězec musí být volána z asynchronní cesty kódu.

Protokolování statistik dotazů SQL Serveru

Tip

Zde uvedený kód pochází z QueryStatisticsLoggerSample.cs.

Nakonec vytvoříme dva průsečíky, které společně posílají statistiky dotazů SQL Serveru do protokolu aplikace. Abychom mohli vygenerovat statistiky, musíme IDbCommandInterceptor udělat dvě věci.

Za prvé, průsečík bude předponovat příkazy s SET STATISTICS IO ON, který sql Serveru říká, aby po použití sady výsledků odeslal klientovi statistiku:

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

Za druhé, interceptor implementuje novou DataReaderClosingAsync metodu, která je volána po DbDataReader dokončení využívání výsledků, ale před uzavřením. Když SQL Server odesílá statistiky, vloží je do druhého výsledku čtenáře, takže v tomto okamžiku průsečík přečte tento výsledek voláním NextResultAsync , které naplní statistiky připojení.

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

    return result;
}

Druhý zachytávání je potřeba k získání statistiky z připojení a jejich zápisu do protokolovacího nástroje aplikace. K tomu použijeme IDbConnectionInterceptor, implementujeme novou ConnectionCreated metodu. ConnectionCreated se volá okamžitě po vytvoření připojení EF Core, takže je možné ho použít k provedení další konfigurace tohoto připojení. V tomto případě průsečík získá ILogger událost a pak se připojí k SqlConnection.InfoMessage události, aby se zprávy protokolují.

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

Důležité

Metody ConnectionCreating a ConnectionCreated metody se volají pouze při vytvoření DbConnectionEF Core . Nebudou volána, pokud aplikace vytvoří DbConnection a předá ji EF Core.

Spuštění některého kódu, který používá tyto průsečíky, zobrazuje statistiky dotazů SQL Serveru v protokolu:

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.

Vylepšení dotazů

EF7 obsahuje mnoho vylepšení překladu dotazů LINQ.

GroupBy jako konečný operátor

Tip

Zde uvedený kód pochází z GroupByFinalOperatorSample.cs.

EF7 podporuje použití GroupBy jako konečný operátor v dotazu. Například tento dotaz LINQ:

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

Při použití SQL Serveru se přeloží na následující SQL:

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

Poznámka:

Tento typ GroupBy se nepřekládá přímo do SQL, takže EF Core provede seskupení vrácených výsledků. Výsledkem však není přenos dalších dat ze serveru.

GroupJoin jako konečný operátor

Tip

Zde uvedený kód pochází z GroupJoinFinalOperatorSample.cs.

EF7 podporuje použití GroupJoin jako konečný operátor v dotazu. Například tento dotaz LINQ:

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

Při použití SQL Serveru se přeloží na následující SQL:

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 entity GroupBy

Tip

Zde uvedený kód pochází z GroupByEntityTypeSample.cs.

EF7 podporuje seskupení podle typu entity. Například tento dotaz LINQ:

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

Při použití SQLite se přeloží na následující SQL:

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]

Mějte na paměti, že seskupení podle jedinečné vlastnosti, například primárního klíče, bude vždy efektivnější než seskupení podle typu entity. Seskupování podle typů entit se ale dá použít pro typy entit s klíči i bez klíčů.

Seskupení podle typu entity s primárním klíčem bude mít vždy za následek jednu skupinu na instanci entity, protože každá entita musí mít jedinečnou hodnotu klíče. Někdy stojí za to přepnout zdroj dotazu, aby seskupování nemuselo být povinné. Například následující dotaz vrátí stejné výsledky jako předchozí dotaz:

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

Tento dotaz se při použití SQLite přeloží na následující SQL:

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

Poddotazy neodkazují na neseskupené sloupce z vnějšího dotazu

Tip

Zde uvedený kód pochází z UngroupedColumnsQuerySample.cs.

V EF Core 6.0 GROUP BY by klauzule odkazovala na sloupce ve vnějším dotazu, což selže s některými databázemi a je neefektivní v jiných. Zvažte například následující dotaz:

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

V EF Core 6.0 na SQL Serveru se to přeložilo 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])

V EF7 je překlad následující:

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]

Kolekce jen pro čtení lze použít pro Contains

Tip

Zde uvedený kód pochází z ReadOnlySetQuerySample.cs.

EF7 podporuje použití Contains , pokud jsou položky, které chcete vyhledat, obsaženy v nebo IReadOnlySet IReadOnlyCollectionnebo IReadOnlyList. Například tento dotaz LINQ:

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

Při použití SQL Serveru se přeloží na následující SQL:

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))

Překlady agregačních funkcí

EF7 zavádí lepší rozšiřitelnost pro poskytovatele pro překlad agregačních funkcí. Výsledkem této a další práce v této oblasti je několik nových překladů mezi poskytovateli, mezi které patří:

Poznámka:

Agregační funkce, které pracují s argumentem IEnumerable , jsou obvykle přeloženy pouze v GroupBy dotazech. Pokud chcete odebrat toto omezení, hlasujte pro podporu prostorových typů ve sloupcích JSON.

Agregační funkce řetězců

Tip

Zde uvedený kód pochází z StringAggregateFunctionsSample.cs.

Dotazy, které používají Join , a Concat jsou nyní přeloženy, pokud je to vhodné. Příklad:

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

Tento dotaz se při použití SQL Serveru přeloží na následující:

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]

V kombinaci s jinými řetězcovými funkcemi umožňují tyto překlady na serveru provádět složitou manipulaci s řetězci. Příklad:

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

Tento dotaz se při použití SQL Serveru přeloží na následující:

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]

Prostorové agregační funkce

Tip

Zde uvedený kód pochází z SpatialAggregateFunctionsSample.cs.

Nyní je možné, aby poskytovatelé databází, kteří podporují NetTopologySuite , přeložily následující prostorové agregační funkce:

Tip

Tyto překlady implementoval tým pro SQL Server a SQLite. U jiných poskytovatelů požádejte správce poskytovatele, aby přidal podporu, pokud byla pro daného poskytovatele implementována.

Příklad:

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

Tento dotaz se při použití SQL Serveru přeloží na následující SQL:

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]

Statistické agregační funkce

Tip

Zde uvedený kód pochází z StatisticalAggregateFunctionsSample.cs.

Překlady SQL Serveru byly implementovány pro následující statistické funkce:

Tip

Tyto překlady implementoval tým pro SQL Server. U jiných poskytovatelů požádejte správce poskytovatele, aby přidal podporu, pokud byla pro daného poskytovatele implementována.

Příklad:

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

Tento dotaz se při použití SQL Serveru přeloží na následující SQL:

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]

Překlad překladu string.IndexOf

Tip

Zde uvedený kód pochází z MiscellaneousTranslationsSample.cs.

EF7 se teď překládá String.IndexOf v dotazech LINQ. Příklad:

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

Tento dotaz se při použití SQL Serveru přeloží na následující SQL:

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 Překlad typů entit

Tip

Zde uvedený kód pochází z MiscellaneousTranslationsSample.cs.

EF7 se teď překládá Object.GetType() v dotazech LINQ. Příklad:

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

Tento dotaz se při použití SQL Serveru s dědičností TPH přeloží na následující SQL:

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'

Všimněte si, že tento dotaz vrací pouze Post instance, které jsou ve skutečnosti typu Post, a ne ty z žádné odvozené typy. To se liší od dotazu, který používá is nebo OfType, který také vrátí instance všech odvozených typů. Představte si například dotaz:

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

To se překládá do jiného SQL:

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

A vrátí obě Post i FeaturedPost entity.

Podpora pro AT TIME ZONE

Tip

Zde uvedený kód pochází z MiscellaneousTranslationsSample.cs.

EF7 zavádí nové AtTimeZone funkce pro DateTime a DateTimeOffset. Tyto funkce se překládají na AT TIME ZONE klauzule ve vygenerovaném SQL. Příklad:

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

Tento dotaz se při použití SQL Serveru přeloží na následující SQL:

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]

Tip

Tyto překlady implementoval tým pro SQL Server. U jiných poskytovatelů požádejte správce poskytovatele, aby přidal podporu, pokud byla pro daného poskytovatele implementována.

Filtrované zahrnutí ve skrytých navigačních panelech

Tip

Zde uvedený kód pochází z MiscellaneousTranslationsSample.cs.

Metody Include lze nyní použít s EF.Property. To umožňuje filtrování a řazení i pro vlastnosti privátní navigace nebo soukromé navigace reprezentované poli. Příklad:

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

To odpovídá:

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

Blog.Posts Nevyžaduje ale, aby byla veřejně přístupná.

Při použití SQL Serveru se oba výše uvedené dotazy překládají 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]

Překlad cosmos pro Regex.IsMatch

Tip

Zde uvedený kód pochází z CosmosQueriesSample.cs.

EF7 podporuje použití Regex.IsMatch v dotazech LINQ ve službě Azure Cosmos DB. Příklad:

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

Přeloží na následující SQL:

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

Vylepšení rozhraní DBContext API a chování

EF7 obsahuje řadu malých vylepšení DbContext a souvisejících tříd.

Tip

Kód ukázek v této části pochází z DbContextApiSample.cs.

Potlačení pro neinicializované vlastnosti DbSet

Veřejné, settable DbSet vlastnosti na objektu DbContext jsou automaticky inicializovány EF Core při DbContext vytváření. Představte si například následující DbContext definici:

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

Vlastnost Blogs bude nastavena na DbSet<Blog> instanci v rámci vytváření DbContext instance. To umožňuje použít kontext pro dotazy bez jakýchkoli dalších kroků.

Po zavedení typů odkazů s možnou hodnotou null v jazyce C# teď kompilátor varuje, že vlastnostBlogs, která není nullable, není inicializována:

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

Toto je falešné varování; vlastnost je nastavena na hodnotu, která není null EF Core. Také deklarování vlastnosti jako nullable způsobí, že upozornění zmizí, ale to není dobrý nápad, protože, koncepčně, vlastnost není nullable a nikdy nebude null.

EF7 obsahuje DiagnosticSuppressor pro DbSet vlastnosti, které DbContext zastaví kompilátor vygenerování tohoto upozornění.

Tip

Tento vzor vznikl ve dnech, kdy byly automatické vlastnosti jazyka C# velmi omezené. U moderního jazyka C# zvažte vytvoření automatických vlastností jen pro čtení a pak je buď inicializujete explicitně v konstruktoru DbContext , nebo v případě potřeby získejte instanci uloženou v mezipaměti DbSet z kontextu. Například public DbSet<Blog> Blogs => Set<Blog>().

Rozlišení zrušení od selhání v protokolech

Aplikace někdy explicitně zruší dotaz nebo jinou operaci databáze. Obvykle se to provádí pomocí CancellationToken předávané metody, která provádí operaci.

V EF Core 6 jsou události protokolované při zrušení operace stejné jako události protokolované v případě selhání operace z nějakého jiného důvodu. EF7 zavádí nové události protokolu speciálně pro zrušené databázové operace. Tyto nové události jsou ve výchozím nastavení protokolovány na Debug úrovni. Následující tabulka uvádí relevantní události a jejich výchozí úrovně protokolu:

Událost Popis Výchozí úroveň protokolování
CoreEventId.QueryIterationFailed Při zpracování výsledků dotazu došlo k chybě. LogLevel.Error
CoreEventId.SaveChangesFailed Při pokusu o uložení změn v databázi došlo k chybě. LogLevel.Error
RelationalEventId.CommandError Při provádění databázového příkazu došlo k chybě. LogLevel.Error
CoreEventId.QueryCanceled Dotaz byl zrušen. LogLevel.Debug
CoreEventId.SaveChangesCanceled Příkaz databáze byl zrušen při pokusu o uložení změn. LogLevel.Debug
RelationalEventId.CommandCanceled Provedení nějakého příkazu DbCommand bylo zrušeno. LogLevel.Debug

Poznámka:

Zrušení se zjistí tak, že se podíváte na výjimku a nekontroluje token zrušení. To znamená, že zrušení, která se neaktivují prostřednictvím tokenu zrušení, budou stále zjištěna a protokolována tímto způsobem.

Nové IProperty a INavigation přetížení pro EntityEntry metody

Kód, který pracuje s modelem EF, bude mít často IProperty vlastnost nebo INavigation navigační metadata. EntityEntry se pak použije k získání vlastnosti nebo navigační hodnoty nebo dotazu na jeho stav. Před ef7 to však vyžadovalo předání názvu vlastnosti nebo navigace metodám EntityEntry, které by pak znovu vyhledat IProperty nebo INavigation. V EF7 IProperty je možné je INavigation předat přímo, aby se zabránilo dalšímu vyhledávání.

Představte si například metodu, která najde všechny stejné entity na stejné úrovni:

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

Tato metoda najde nadřazenou položku dané entity a pak předá inverzní metodu INavigation Collection nadřazené položky. Tato metadata se pak použijí k vrácení všech elementů na stejné úrovni daného nadřazeného objektu. Tady je příklad použití:


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

A výstup:

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 pro typy entit sdíleného typu

EF Core může použít stejný typ CLR pro více různých typů entit. Tyto typy entit se označují jako "typy entit sdíleného typu" a často se používají k mapování typu slovníku s páry klíč/hodnota, které se používají pro vlastnosti typu entity. BuildMetadata Například typ entity lze definovat bez definování vyhrazeného 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");
    });

Všimněte si, že typ entity sdíleného typu musí být pojmenován – v tomto případě je BuildMetadatanázev . K těmto typům entit se pak přistupuje pomocí DbSet typu entity, který se získá pomocí názvu. Příklad:

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

DbSet se použít ke sledování instancí entit:

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

A spouštět dotazy:

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

Nyní v EF7 existuje také Entry metoda, na DbSet které lze použít k získání stavu instance, i když ještě není sledován. Příklad:

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

ContextInitialized je nyní zaprotokolován jako Debug

V EF7 ContextInitialized se událost protokoluje na Debug úrovni. Příklad:

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

V předchozích verzích se protokoloval na Information úrovni. Příklad:

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

V případě potřeby je možné úroveň protokolu změnit zpět na Information:

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

IEntityEntryGraphIterator je veřejně použitelná

V EF7 IEntityEntryGraphIterator může služba používat aplikace. To je služba používaná interně při zjišťování grafu entit ke sledování, a také pomocí TrackGraph. Tady je příklad, který iteruje nad všemi entitami dostupnými z některé počáteční entity:

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();

Poznámka:

  • Iterátor přestane přecházet z daného uzlu, když se vrátí falsedelegát zpětného volání . Tento příklad sleduje navštívené entity a vrací se false , když už byla entita navštívena. Tím se zabrání nekonečným smyčkám vyplývajícím z cyklů v grafu.
  • Objekt EntityEntryGraphNode<TState> umožňuje předání stavu, aniž by ho zachytálo do delegáta.
  • U každého uzlu, který navštívil jiný než první, se zjistil uzel a navigace, přes který byl zjištěn, se předá zpětnému volání.

Vylepšení vytváření modelů

EF7 obsahuje řadu malých vylepšení při sestavování modelů.

Tip

Kód ukázek v této části pochází z ModelBuildingSample.cs.

Indexy můžou být vzestupné nebo sestupné.

Ve výchozím nastavení EF Core vytváří vzestupné indexy. EF7 také podporuje vytváření sestupných indexů. Příklad:

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

Nebo pomocí atributu Index mapování:

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

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

To je zřídka užitečné pro indexy v jednom sloupci, protože databáze může použít stejný index pro řazení v obou směrech. Nejedná se však o případ složených indexů ve více sloupcích, kde pořadí jednotlivých sloupců může být důležité. EF Core to podporuje tím, že umožňuje, aby pro každý sloupec bylo definované různé řazení více sloupců. Příklad:

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

Nebo pomocí atributu mapování:

[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();
}

Výsledkem je následující SQL při použití SQL Serveru:

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

Nakonec lze vytvořit více indexů ve stejné uspořádané sadě sloupců tak, že pojmenuje indexy. Příklad:

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

Nebo pomocí atributů mapování:

[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();
}

Tím se na SQL Serveru vygeneruje následující SQL:

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

Atribut mapování pro složené klíče

EF7 zavádí nový atribut mapování (neboli "datové poznámky") pro zadání vlastnosti primárního klíče nebo vlastností libovolného typu entity. Na rozdíl od třídy PrimaryKeyAttribute typu entity je umístěna na rozdíl od System.ComponentModel.DataAnnotations.KeyAttributetřídy typu entity, nikoli na vlastnost klíče. Příklad:

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

Díky tomu se přirozeně hodí pro definování složených klíčů:

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

Definování indexu třídy také znamená, že lze použít k určení privátních vlastností nebo polí jako klíčů, i když by se tyto vlastnosti při sestavování modelu EF obvykle ignorovaly. Příklad:

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

DeleteBehavior atribut mapování

EF7 zavádí atribut mapování (neboli "poznámku DeleteBehavior k datům") pro určení relace. Požadované relace se například vytvářejí DeleteBehavior.Cascade ve výchozím nastavení. Toto nastavení lze ve výchozím nastavení změnit DeleteBehavior.NoAction pomocí DeleteBehaviorAttribute:

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

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

Tím se kaskádové odstranění pro relaci Blog-Příspěvky zakáže.

Vlastnosti mapované na různé názvy sloupců

Některé vzory mapování mají za následek, že se stejná vlastnost CLR mapuje na sloupec v každé z několika různých tabulek. EF7 umožňuje těmto sloupcům mít různé názvy. Představte si například jednoduchou hierarchii dědičnosti:

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

U strategie mapování dědičnosti TPT se tyto typy mapují na tři tabulky. Sloupec primárního klíče v každé tabulce ale může mít jiný název. Příklad:

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

EF7 umožňuje konfiguraci tohoto mapování pomocí tvůrce vnořených tabulek:

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

Pomocí mapování Breed dědičnosti TPC lze vlastnost také mapovat na různé názvy sloupců v různých tabulkách. Představte si například následující tabulky 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])
);

EF7 podporuje toto mapování tabulek:

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

Jednosměrné relace M:N

EF7 podporuje relace M:N, u kterých jedna strana nebo druhá nemá navigační vlastnost. Představte si Post například a Tag typy:

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

Všimněte si, že typ Post má navigační vlastnost pro seznam značek, ale Tag typ nemá navigační vlastnost pro příspěvky. V SYSTÉMU EF7 je stále možné tuto možnost nakonfigurovat jako relaci M:N, což umožňuje použití stejného Tag objektu pro mnoho různých příspěvků. Příklad:

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

Výsledkem je mapování na příslušnou tabulku spojení:

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 vztah lze použít jako M:N normálním způsobem. Můžete například vložit některé příspěvky, které sdílejí různé značky ze společné sady:

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();

Dělení entity

Rozdělení entit mapuje jeden typ entity na více tabulek. Představte si například databázi se třemi tabulkami, které obsahují zákaznická data:

  • Tabulka Customers s informacemi o zákazníci
  • Tabulka PhoneNumbers telefonního čísla zákazníka
  • Tabulka Addresses adresy zákazníka

Tady jsou definice pro tyto tabulky v SQL Serveru:

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ždá z těchto tabulek by se obvykle mapovala na vlastní typ entity s relacemi mezi těmito typy. Pokud se ale všechny tři tabulky vždy používají společně, může být vhodnější je namapovat všechny na jeden typ entity. Příklad:

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

Toho dosáhnete v EF7 voláním SplitToTable pro každé rozdělení v typu entity. Například následující kód rozdělí Customer typ entity na Customers, PhoneNumbersa Addresses tabulky uvedené výše:

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

Všimněte si také, že v případě potřeby je možné pro každou z tabulek zadat jiné názvy sloupců primárního klíče.

Řetězce UTF-8 SQL Serveru

Řetězce unicode SQL Serveru reprezentované datovými nchar nvarchar typy jsou uloženy jako UTF-16. Kromě toho char varchar se datové typy používají k ukládání řetězců, které nejsou unicode, s podporou různých znakových sad.

Počínaje SQL Serverem 2019 char je možné místo toho ukládat řetězce Unicode s kódováním UTF-8.varchar Toho dosáhnete nastavením jedné z kolací UTF-8. Například následující kód nakonfiguruje pro sloupec proměnnou délku řetězce UTF-8 systému CommentText SQL Server:

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

Tato konfigurace vygeneruje následující definici sloupce SQL Serveru:

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])
);

Dočasné tabulky podporují vlastněné entity.

Mapování dočasných tabulek EF Core SQL Serveru bylo v EF7 vylepšeno tak, aby podporovalo sdílení tabulek. Výchozí mapování pro vlastněné jednotlivé entity nejčastěji používá sdílení tabulek.

Představte si například typ Employee entity vlastníka a vlastní typ EmployeeInfoentity:

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

Pokud jsou tyto typy mapovány na stejnou tabulku, pak v EF7 lze vytvořit dočasnou tabulku:

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

Poznámka:

Usnadnění této konfigurace je sledováno pomocí problému č. 29303. Hlasujte pro tento problém, pokud byste chtěli vidět implementované.

Vylepšené generování hodnot

EF7 obsahuje dvě významná vylepšení automatického generování hodnot pro vlastnosti klíče.

Tip

Kód ukázek v této části pochází z ValueGenerationSample.cs.

Generování hodnot pro chráněné typy DDD

V návrhu řízeném doménou (DDD) můžou "strážené klíče" zlepšit bezpečnost typů klíčových vlastností. Toho dosáhnete zabalením typu klíče do jiného typu, který je specifický pro použití klíče. Například následující kód definuje ProductId typ pro kódy Product Key a CategoryId typ pro klíče kategorií.

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

Ty se pak použijí v Product typech entit a Category v těchto typech:

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

To znemožňuje náhodné přiřazení ID kategorie k produktu nebo naopak.

Upozorňující

Stejně jako u mnoha konceptů DDD přichází tato vylepšená bezpečnost typů na úkor další složitosti kódu. Je vhodné zvážit, jestli například přiřazení ID produktu do kategorie je něco, co se někdy stane. Udržování jednoduchých věcí může být pro základ kódu celkově přínosnější.

Typy strážených klíčů, které jsou zde uvedeny, zabalí int hodnoty klíče, což znamená, že hodnoty celého čísla se použijí v mapovaných databázových tabulkách. Toho dosáhnete definováním převaděčů hodnot pro tyto typy:

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))
    {
    }
}

Poznámka:

Kód zde používá struct typy. To znamená, že mají odpovídající sémantiku hodnot pro použití jako klíče. Pokud class se místo toho používají typy, musí buď přepsat sémantiku rovnosti, nebo také určit porovnávač hodnot.

V EF7 můžou typy klíčů založené na převaděčích hodnot používat automaticky generované hodnoty klíče, pokud to základní typ podporuje. To se konfiguruje běžným způsobem pomocí ValueGeneratedOnAdd:

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

Ve výchozím nastavení se při použití s SQL Serverem zobrazí IDENTITY sloupce:

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

Které se používají normálním způsobem generování klíčových hodnot při vkládání entit:

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;

Generování klíčů založené na sekvenci pro SQL Server

EF Core podporuje generování klíčových hodnot pomocí sloupců SQL Serveru IDENTITY nebo vzor Hi-Lo založený na blocích klíčů vygenerovaných sekvencí databáze. EF7 zavádí podporu pro sekvenci databáze připojenou k výchozímu omezení sloupce klíče. V nejjednodušší podobě to vyžaduje, aby EF Core používala posloupnost pro vlastnost klíče:

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

Výsledkem je, že se v databázi definuje posloupnost:

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

Který se pak použije ve výchozím omezení klíčového sloupce:

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

Poznámka:

Tato forma generování klíčů se ve výchozím nastavení používá pro vygenerované klíče v hierarchiích typů entit pomocí strategie mapování TPC.

V případě potřeby může mít sekvence jiný název a schéma. Příklad:

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

Další konfigurace sekvence se vytvoří tak, že ji nakonfigurujete explicitně v modelu. Příklad:

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

Vylepšení nástrojů pro migraci

EF7 obsahuje dvě významná vylepšení při používání nástrojů příkazového řádku migrace EF Core.

UseSqlServer atd. přijměte hodnotu null.

Je velmi běžné číst připojovací řetězec z konfiguračního souboru a pak předat připojovací řetězec do UseSqlServer, UseSqlitenebo ekvivalentní metodu pro jiného poskytovatele. Příklad:

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

Při použití migrací je také běžné předat připojovací řetězec. Příklad:

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

Nebo při použití sady migrací.

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

V tomto případě se i když se připojovací řetězec číst z konfigurace nepoužívá, spouštěcí kód aplikace se stále pokouší číst z konfigurace a předat ho UseSqlServer. Pokud konfigurace není k dispozici, výsledkem je předání hodnoty null .UseSqlServer V EF7 je to povolené, pokud je připojovací řetězec nakonec nastaven později, například předáním --connection nástroje příkazového řádku.

Poznámka:

Tato změna byla provedena pro UseSqlServer a UseSqlite. U jiných poskytovatelů se obraťte na správce poskytovatele, aby provedl ekvivalentní změnu, pokud ještě pro daného poskytovatele nebyla provedena.

Zjištění, kdy jsou nástroje spuštěné

EF Core spouští kód aplikace, když dotnet-ef se používají příkazy PowerShellu . Někdy může být nutné zjistit tuto situaci, aby se zabránilo spuštění nevhodného kódu v době návrhu. Například kód, který automaticky použije migrace při spuštění, by to pravděpodobně neměl provést v době návrhu. V EF7 to lze zjistit pomocí příznaku EF.IsDesignTime :

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

EF Core nastaví IsDesignTime , kdy true je kód aplikace spuštěný jménem nástrojů.

Vylepšení výkonu pro proxy servery

EF Core podporuje dynamicky generované proxy servery pro opožděné načítání a sledování změn. EF7 obsahuje dvě vylepšení výkonu při použití těchto proxy serverů:

  • Typy proxy serveru se teď vytvářejí opožděně. To znamená, že počáteční doba vytváření modelu při použití proxy serverů může být s EF7 výrazně rychlejší než u EF Core 6.0.
  • Proxy servery se teď dají používat s kompilovanými modely.

Tady jsou některé výsledky výkonu modelu s typy entit 449, 6390 vlastností a 720 relací.

Scénář metoda Střední hodnota Chyba Směrodatná odchylka
EF Core 6.0 bez proxy serverů TimeToFirstQuery 1.085 s 0.0083 s 0.0167 s
EF Core 6.0 s proxy servery pro sledování změn TimeToFirstQuery 13.01 s 0.2040 s 0.4110 s
EF Core 7.0 bez proxy serverů TimeToFirstQuery 1.442 s 0.0134 s 0.0272 s
EF Core 7.0 s proxy servery pro sledování změn TimeToFirstQuery 1.446 s 0.0160 s 0.0323 s
EF Core 7.0 s proxy servery pro sledování změn a kompilovaným modelem TimeToFirstQuery 0.162 s 0.0062 s 0.0125 s

V tomto případě tedy může být model s proxy pro sledování změn připravený k provedení prvního dotazu 80krát rychleji v EF7, než bylo možné s EF Core 6.0.

První třída model Windows Forms datové vazby

Tým model Windows Forms výrazně vylepšil prostředí návrháře sady Visual Studio. To zahrnuje nová prostředí pro datovou vazbu , která se dobře integruje s EF Core.

Stručně řečeno, nové prostředí poskytuje Visual Studio U.I. pro vytvoření ObjectDataSource:

Zvolte typ zdroje dat Kategorie.

To pak může být vázáno na EF Core DbSet pomocí některého jednoduchého kódu:

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

Podívejte se na téma Začínáme s model Windows Forms, kde najdete kompletní návod a ukázkovou aplikaci WinForms ke stažení.