Sdílet prostřednictvím


Poskytovatel Azure Cosmos DB pro EF Core

Tento poskytovatel databáze umožňuje použít Entity Framework Core s Azure Cosmos DB. Tento poskytovatel je součástí projektu Entity Framework Core.

Před přečtením této části důrazně doporučujeme seznámit se s dokumentací ke službě Azure Cosmos DB.

Poznámka:

Tento poskytovatel funguje jenom se službou Azure Cosmos DB for NoSQL.

Instalace

Nainstalujte si balíček Microsoft.EntityFrameworkCore.Cosmos NuGet.

dotnet add package Microsoft.EntityFrameworkCore.Cosmos

Začínáme

Tip

Ukázku pro tento článek najdete na GitHubu.

Stejně jako u jiných poskytovatelů je prvním krokem volání UseCosmos:

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    => optionsBuilder.UseCosmos(
        "https://localhost:8081",
        "C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==",
        databaseName: "OrdersDB");

Upozorňující

Koncový bod a klíč jsou zde kvůli jednoduchosti pevně zakódovány, ale v produkční aplikaci by měly být bezpečně uloženy. Další způsoby připojení ke službě Azure Cosmos DB najdete v tématu Připojení a ověřování .

V tomto příkladu Order je jednoduchá entita s odkazem na vlastněný typ StreetAddress.

public class Order
{
    public int Id { get; set; }
    public int? TrackingNumber { get; set; }
    public string PartitionKey { get; set; }
    public StreetAddress ShippingAddress { get; set; }
}
public class StreetAddress
{
    public string Street { get; set; }
    public string City { get; set; }
}

Ukládání a dotazování dat se řídí běžným vzorem EF:

using (var context = new OrderContext())
{
    await context.Database.EnsureDeletedAsync();
    await context.Database.EnsureCreatedAsync();

    context.Add(
        new Order
        {
            Id = 1, ShippingAddress = new StreetAddress { City = "London", Street = "221 B Baker St" }, PartitionKey = "1"
        });

    await context.SaveChangesAsync();
}

using (var context = new OrderContext())
{
    var order = await context.Orders.FirstAsync();
    Console.WriteLine($"First order will ship to: {order.ShippingAddress.Street}, {order.ShippingAddress.City}");
    Console.WriteLine();
}

Důležité

Volání EnsureCreatedAsync je nezbytné k vytvoření požadovaných kontejnerů a k vložení počátečních dat, pokud se v modelu nacházejí. EnsureCreatedAsync by se však mělo volat jen během nasazování a nikoli při běžném provozu, protože může způsobit problémy s výkonem.

Připojení a ověřování

Zprostředkovatel služby Azure Cosmos DB pro EF Core má více přetížení metody UseCosmos . Tato přetížení podporují různé způsoby, jak lze vytvořit připojení k databázi, a různé způsoby zajištění zabezpečení připojení.

Důležité

Ujistěte se, že rozumíte zabezpečenému přístupu k datům ve službě Azure Cosmos DB, abyste porozuměli důsledkům zabezpečení a osvědčeným postupům při používání každého přetížení UseCosmos této metody.

Mechanismus připojení UseCosmos – přetížení Více informací
Koncový bod a klíč účtu UseCosmos<DbContext>(accountEndpoint, accountKey, databaseName) Primární/sekundární klíče
Koncový bod účtu a token UseCosmos<DbContext>(accountEndpoint, tokenCredential, databaseName) Tokeny prostředků
Connection string UseCosmos<DbContext>(connectionString, databaseName) Práce s klíči účtu a připojovací řetězec

Dotazy

LINQ – dotazy

Dotazy LINQ EF Core je možné spouštět ve službě Azure Cosmos DB stejným způsobem jako u jiných poskytovatelů databází. Příklad:

var stringResults = await context.Triangles.Where(
        e => e.Name.Length > 4
             && e.Name.Trim().ToLower() != "obtuse"
             && e.Name.TrimStart().Substring(2, 2).Equals("uT", StringComparison.OrdinalIgnoreCase))
    .ToListAsync();

Poznámka:

Zprostředkovatel Azure Cosmos DB nepřekládá stejnou sadu dotazů LINQ jako ostatní zprostředkovatelé. Další informace najdete v tématu Omezení .

Dotazy SQL

Dotazy lze také zapsat přímo v SQL. Příklad:

var maxAngle = 60;
var results = await context.Triangles.FromSqlRaw(
        @"SELECT * FROM root c WHERE c[""Angle1""] <= {0} OR c[""Angle2""] <= {0}", maxAngle)
    .ToListAsync();

Výsledkem tohoto dotazu je následující spuštění dotazu:

SELECT c
FROM (
    SELECT * FROM root c WHERE c["Angle1"] <= @p0 OR c["Angle2"] <= @p0
) c

Stejně jako u relačních FromSql dotazů je možné ručně napsaný JAZYK SQL dále skládat pomocí operátorů LINQ. Příklad:

var maxAngle = 60;
var results = await context.Triangles.FromSqlRaw(
        @"SELECT * FROM root c WHERE c[""Angle1""] <= {0} OR c[""Angle2""] <= {0}", maxAngle)
    .Where(e => e.InsertedOn <= DateTime.UtcNow)
    .Select(e => e.Angle1).Distinct()
    .ToListAsync();

Tato kombinace JAZYKa SQL a LINQ se překládá na:

SELECT DISTINCT c["Angle1"]
FROM (
    SELECT * FROM root c WHERE c["Angle1"] <= @p0 OR c["Angle2"] <= @p0
) c
WHERE (c["InsertedOn"] <= GetCurrentDateTime())

Možnosti služby Azure Cosmos DB

Je také možné nakonfigurovat poskytovatele služby Azure Cosmos DB s jedním připojovací řetězec a zadat další možnosti pro přizpůsobení připojení:

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    => optionsBuilder.UseCosmos(
        "AccountEndpoint=https://localhost:8081/;AccountKey=C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==",
        databaseName: "OptionsDB",
        options =>
        {
            options.ConnectionMode(ConnectionMode.Gateway);
            options.WebProxy(new WebProxy());
            options.LimitToEndpoint();
            options.Region(Regions.AustraliaCentral);
            options.GatewayModeMaxConnectionLimit(32);
            options.MaxRequestsPerTcpConnection(8);
            options.MaxTcpConnectionsPerEndpoint(16);
            options.IdleTcpConnectionTimeout(TimeSpan.FromMinutes(1));
            options.OpenTcpConnectionTimeout(TimeSpan.FromMinutes(1));
            options.RequestTimeout(TimeSpan.FromMinutes(1));
        });

Tip

Výše uvedený kód ukazuje možné možnosti. Není zamýšleno, aby se všechny používaly najednou! Podrobný popis účinku jednotlivých výše uvedených možností najdete v dokumentaci k možnostem Azure Cosmos DB.

Přizpůsobení modelu specifická pro Cosmos

Standardně jsou všechny typy entit namapovány na stejný kontejner pojmenovaný po odvozeném kontextu (v tomto případě "OrderContext"). Pokud chcete změnit výchozí název kontejneru, použijte HasDefaultContainer:

modelBuilder.HasDefaultContainer("Store");

K namapování typu entity na jiný kontejner použijte ToContainer:

modelBuilder.Entity<Order>()
    .ToContainer("Orders");

Kvůli identifikaci typu entity, kterou daná položka reprezentuje, přidává EF Core hodnotu diskriminátoru, i když neexistují žádné odvozené typy entit. Název a hodnotu tohoto diskriminátoru lze změnit.

Pokud ve stejném kontejneru nebude nikdy uložen žádný jiný typ entity, může být diskriminátor odebrán voláním HasNo Discriminatoror:

modelBuilder.Entity<Order>()
    .HasNoDiscriminator();

Klíče oddílu

Ve výchozím nastavení EF Core vytvoří kontejnery s klíčem oddílu nastaveným na "__partitionKey" bez zadání jakékoli hodnoty pro ni při vkládání položek. Pokud ale chcete plně využít možnosti výkonu služby Azure Cosmos DB, měli byste použít pečlivě vybraný klíč oddílu. Lze ho nakonfigurovat voláním HasPartitionKey:

modelBuilder.Entity<Order>()
    .HasPartitionKey(o => o.PartitionKey);

Poznámka:

Vlastnost klíče oddílu může být libovolného typu, pokud je převedena na řetězec.

Po nakonfigurování by vlastnost klíče oddílu měla mít vždy jinou hodnotu než null. Přidáním volání WithPartitionKey lze dotaz vytvořit s jedním oddílem.

using (var context = new OrderContext())
{
    context.Add(
        new Order
        {
            Id = 2, ShippingAddress = new StreetAddress { City = "New York", Street = "11 Wall Street" }, PartitionKey = "2"
        });

    await context.SaveChangesAsync();
}

using (var context = new OrderContext())
{
    var order = await context.Orders.WithPartitionKey("2").LastAsync();
    Console.WriteLine($"Last order will ship to: {order.ShippingAddress.Street}, {order.ShippingAddress.City}");
    Console.WriteLine();
}

Obecně se doporučuje přidat klíč oddílu do primárního klíče, protože to nejlépe odráží sémantiku serveru a umožňuje některé optimalizace, například u FindAsync.

Zřízená propustnost

Pokud k vytvoření databáze nebo kontejnerů Azure Cosmos DB použijete EF Core, můžete nakonfigurovat zřízenou propustnost pro databázi voláním CosmosModelBuilderExtensions.HasAutoscaleThroughput nebo CosmosModelBuilderExtensions.HasManualThroughput. Příklad:

modelBuilder.HasManualThroughput(2000);
modelBuilder.HasAutoscaleThroughput(4000);

Pokud chcete pro kontejner nakonfigurovat zřízenou propustnost, volejte CosmosEntityTypeBuilderExtensions.HasAutoscaleThroughput nebo CosmosEntityTypeBuilderExtensions.HasManualThroughput. Příklad:

modelBuilder.Entity<Family>(
    entityTypeBuilder =>
    {
        entityTypeBuilder.HasManualThroughput(5000);
        entityTypeBuilder.HasAutoscaleThroughput(3000);
    });

Time-to-live

Typy entit v modelu Azure Cosmos DB se teď dají nakonfigurovat s výchozím časem naživo. Příklad:

modelBuilder.Entity<Hamlet>().HasDefaultTimeToLive(3600);

Nebo pro analytické úložiště:

modelBuilder.Entity<Hamlet>().HasAnalyticalStoreTimeToLive(3600);

Pro jednotlivé entity je možné nastavit časový limit pro jednotlivé entity pomocí vlastnosti mapované na hodnotu ttl v dokumentu JSON. Příklad:

modelBuilder.Entity<Village>()
    .HasDefaultTimeToLive(3600)
    .Property(e => e.TimeToLive)
    .ToJsonProperty("ttl");

Poznámka:

Výchozí hodnota time-to-live musí být nakonfigurovaná pro typ entity, aby "ttl" měla jakýkoli vliv. Další informace najdete v tématu TTL (Time to Live) ve službě Azure Cosmos DB .

Vlastnost time-to-live je pak nastavena před uložením entity. Příklad:

var village = new Village { Id = "DN41", Name = "Healing", TimeToLive = 60 };
context.Add(village);
await context.SaveChangesAsync();

Vlastnost time-to-live může být stínová vlastnost , aby se zabránilo znečisťování entity domény s obavami databáze. Příklad:

modelBuilder.Entity<Hamlet>()
    .HasDefaultTimeToLive(3600)
    .Property<int>("TimeToLive")
    .ToJsonProperty("ttl");

Vlastnost stínového času naživo je pak nastavena přístupem ke sledované entitě. Příklad:

var hamlet = new Hamlet { Id = "DN37", Name = "Irby" };
context.Add(hamlet);
context.Entry(hamlet).Property("TimeToLive").CurrentValue = 60;
await context.SaveChangesAsync();

Vložené entity

Poznámka:

Související typy entit jsou ve výchozím nastavení nakonfigurované jako vlastněné. Voláním ModelBuilder.Entity tomu můžete u konkrétního typu entity zabránit.

Ve službě Azure Cosmos DB jsou vlastněné entity vložené do stejné položky jako vlastník. Ke změně názvu vlastnosti použijte ToJsonProperty:

modelBuilder.Entity<Order>().OwnsOne(
    o => o.ShippingAddress,
    sa =>
    {
        sa.ToJsonProperty("Address");
        sa.Property(p => p.Street).ToJsonProperty("ShipsToStreet");
        sa.Property(p => p.City).ToJsonProperty("ShipsToCity");
    });

Při této konfiguraci se objednávka z předchozího příkladu uloží takto:

{
    "Id": 1,
    "PartitionKey": "1",
    "TrackingNumber": null,
    "id": "1",
    "Address": {
        "ShipsToCity": "London",
        "ShipsToStreet": "221 B Baker St"
    },
    "_rid": "6QEKAM+BOOABAAAAAAAAAA==",
    "_self": "dbs/6QEKAA==/colls/6QEKAM+BOOA=/docs/6QEKAM+BOOABAAAAAAAAAA==/",
    "_etag": "\"00000000-0000-0000-683c-692e763901d5\"",
    "_attachments": "attachments/",
    "_ts": 1568163674
}

Kolekce vlastněných entit jsou také vložené. V dalším příkladu použijeme třídu Distributor s kolekcí StreetAddress:

public class Distributor
{
    public int Id { get; set; }
    public string ETag { get; set; }
    public ICollection<StreetAddress> ShippingCenters { get; set; }
}

Vlastněné entity nemusí poskytovat explicitní hodnoty klíče, které se mají uložit:

var distributor = new Distributor
{
    Id = 1,
    ShippingCenters = new HashSet<StreetAddress>
    {
        new StreetAddress { City = "Phoenix", Street = "500 S 48th Street" },
        new StreetAddress { City = "Anaheim", Street = "5650 Dolly Ave" }
    }
};

using (var context = new OrderContext())
{
    context.Add(distributor);

    await context.SaveChangesAsync();
}

Budou zachovány takto:

{
    "Id": 1,
    "Discriminator": "Distributor",
    "id": "Distributor|1",
    "ShippingCenters": [
        {
            "City": "Phoenix",
            "Street": "500 S 48th Street"
        },
        {
            "City": "Anaheim",
            "Street": "5650 Dolly Ave"
        }
    ],
    "_rid": "6QEKANzISj0BAAAAAAAAAA==",
    "_self": "dbs/6QEKAA==/colls/6QEKANzISj0=/docs/6QEKANzISj0BAAAAAAAAAA==/",
    "_etag": "\"00000000-0000-0000-683c-7b2b439701d5\"",
    "_attachments": "attachments/",
    "_ts": 1568163705
}

EF Core musí mít interně pro všechny sledované entity vždy jedinečné klíčové hodnoty. Primární klíč standardně vytvořený pro kolekce vlastněných typů sestává z vlastností cizího klíče odkazujících na vlastníka a z vlastnosti int odpovídající indexu v poli JSON. K načtení položky s těmito hodnotami lze použít rozhraní API:

using (var context = new OrderContext())
{
    var firstDistributor = await context.Distributors.FirstAsync();
    Console.WriteLine($"Number of shipping centers: {firstDistributor.ShippingCenters.Count}");

    var addressEntry = context.Entry(firstDistributor.ShippingCenters.First());
    var addressPKProperties = addressEntry.Metadata.FindPrimaryKey().Properties;

    Console.WriteLine(
        $"First shipping center PK: ({addressEntry.Property(addressPKProperties[0].Name).CurrentValue}, {addressEntry.Property(addressPKProperties[1].Name).CurrentValue})");
    Console.WriteLine();
}

Tip

V případě potřeby lze výchozí primární klíč pro typy vlastněných entit změnit, ale hodnoty klíčů by měly být zadány explicitně.

Kolekce primitivních typů

Kolekce podporovaných primitivních typů, například string a int, jsou zjišťovány a mapovány automaticky. Podporované kolekce jsou všechny typy, které implementují IReadOnlyList<T> nebo IReadOnlyDictionary<TKey,TValue>. Představte si například tento typ entity:

public class Book
{
    public Guid Id { get; set; }
    public string Title { get; set; }
    public IList<string> Quotes { get; set; }
    public IDictionary<string, string> Notes { get; set; }
}

Jak seznam, tak slovník lze naplnit a vložit do databáze běžným způsobem:

using var context = new BooksContext();

var book = new Book
{
    Title = "How It Works: Incredible History",
    Quotes = new List<string>
    {
        "Thomas (Tommy) Flowers was the British engineer behind the design of the Colossus computer.",
        "Invented originally for Guinness, plastic widgets are nitrogen-filled spheres.",
        "For 20 years after its introduction in 1979, the Walkman dominated the personal stereo market."
    },
    Notes = new Dictionary<string, string>
    {
        { "121", "Fridges" },
        { "144", "Peter Higgs" },
        { "48", "Saint Mark's Basilica" },
        { "36", "The Terracotta Army" }
    }
};

context.Add(book);
await context.SaveChangesAsync();

Výsledkem je následující dokument JSON:

{
    "Id": "0b32283e-22a8-4103-bb4f-6052604868bd",
    "Discriminator": "Book",
    "Notes": {
        "36": "The Terracotta Army",
        "48": "Saint Mark's Basilica",
        "121": "Fridges",
        "144": "Peter Higgs"
    },
    "Quotes": [
        "Thomas (Tommy) Flowers was the British engineer behind the design of the Colossus computer.",
        "Invented originally for Guinness, plastic widgets are nitrogen-filled spheres.",
        "For 20 years after its introduction in 1979, the Walkman dominated the personal stereo market."
    ],
    "Title": "How It Works: Incredible History",
    "id": "Book|0b32283e-22a8-4103-bb4f-6052604868bd",
    "_rid": "t-E3AIxaencBAAAAAAAAAA==",
    "_self": "dbs/t-E3AA==/colls/t-E3AIxaenc=/docs/t-E3AIxaencBAAAAAAAAAA==/",
    "_etag": "\"00000000-0000-0000-9b50-fc769dc901d7\"",
    "_attachments": "attachments/",
    "_ts": 1630075016
}

Tyto kolekce je pak možné aktualizovat, a to opět běžným způsobem:

book.Quotes.Add("Pressing the emergency button lowered the rods again.");
book.Notes["48"] = "Chiesa d'Oro";

await context.SaveChangesAsync();

Omezení:

  • Podporují se jen slovníky s řetězcovými klíči.
  • Dotazování na obsah primitivních kolekcí se v současné době nepodporuje. Pokud jsou pro vás tyto funkce důležité, hlasujte pro #16926, #25700 a #25701.

Práce s odpojenými entitami

Každá položka musí mít hodnotu id, která je pro daný klíč oddílu jedinečná. EF Core standardně generuje tuto hodnotu tak, že zřetězí diskriminátor a hodnoty primárního klíče, přičemž jako oddělovač použije znak „|“. Hodnoty klíče se generují jen v případě, kdy entita přejde do stavu Added. To může představovat problém při připojování entit, pokud u typu .NET nemají vlastnost id k uložení hodnoty.

Toto omezení obejdete tak, že hodnotu id vytvoříte a nastavíte ručně, nebo entitu nejprve označíte jako přidanou a pak ji změníte do požadovaného stavu:

using (var context = new OrderContext())
{
    var distributorEntry = context.Add(distributor);
    distributorEntry.State = EntityState.Unchanged;

    distributor.ShippingCenters.Remove(distributor.ShippingCenters.Last());

    await context.SaveChangesAsync();
}

using (var context = new OrderContext())
{
    var firstDistributor = await context.Distributors.FirstAsync();
    Console.WriteLine($"Number of shipping centers is now: {firstDistributor.ShippingCenters.Count}");

    var distributorEntry = context.Entry(firstDistributor);
    var idProperty = distributorEntry.Property<string>("__id");
    Console.WriteLine($"The distributor 'id' is: {idProperty.CurrentValue}");
}

Zde je výsledný JSON:

{
    "Id": 1,
    "Discriminator": "Distributor",
    "id": "Distributor|1",
    "ShippingCenters": [
        {
            "City": "Phoenix",
            "Street": "500 S 48th Street"
        }
    ],
    "_rid": "JBwtAN8oNYEBAAAAAAAAAA==",
    "_self": "dbs/JBwtAA==/colls/JBwtAN8oNYE=/docs/JBwtAN8oNYEBAAAAAAAAAA==/",
    "_etag": "\"00000000-0000-0000-9377-d7a1ae7c01d5\"",
    "_attachments": "attachments/",
    "_ts": 1572917100
}

Optimistické uzamykání pomocí eTags

Pokud chcete nějaký typ entity nakonfigurovat tak, aby se používalo optimistické uzamykání, volejte UseETagConcurrency. Toto volání vytvoří vlastnost _etag ve stínovém stavu a nastaví ji jako token uzamykání.

modelBuilder.Entity<Order>()
    .UseETagConcurrency();

Pokud chcete usnadnit řešení chyb uzamykání, můžete pomocí IsETagConcurrency namapovat eTag na některou vlastnost CLR.

modelBuilder.Entity<Distributor>()
    .Property(d => d.ETag)
    .IsETagConcurrency();