Настройка модели с помощью поставщика EF Core Azure Cosmos DB
Контейнеры и типы сущностей
В Azure Cosmos DB документы JSON хранятся в контейнерах. В отличие от таблиц в реляционных базах данных контейнеры Azure Cosmos DB могут содержать документы с различными фигурами. Контейнер не накладывает единую схему на свои документы. Однако различные параметры конфигурации определяются на уровне контейнера и, следовательно, влияют на все документы, содержащиеся в нем. Дополнительные сведения см. в документации по Azure Cosmos DB по контейнерам .
По умолчанию EF сопоставляет все типы сущностей с одинаковым контейнером; Обычно это хорошее значение по умолчанию с точки зрения производительности и ценообразования. Контейнер по умолчанию называется типом контекста .NET (OrderContext
в данном случае). Чтобы изменить имя контейнера по умолчанию, используйте HasDefaultContainerследующую команду:
modelBuilder.HasDefaultContainer("Store");
Сопоставить тип сущности с другим контейнером можно с помощью ToContainer:
modelBuilder.Entity<Order>().ToContainer("Orders");
Прежде чем сопоставлять типы сущностей с различными контейнерами, убедитесь, что вы понимаете потенциальные последствия для производительности и ценообразования (например, в отношении выделенной и общей пропускной способности); Дополнительные сведения см. в документации по Azure Cosmos DB.
Идентификаторы и ключи
Azure Cosmos DB требует, чтобы все документы имели id
свойство JSON, которое однозначно идентифицирует их. Как и другие поставщики EF, поставщик EF Azure Cosmos DB попытается найти свойство с именем Id
или <type name>Id
, а также настроить это свойство в качестве ключа типа сущности, сопоставив его со свойством id
JSON. Вы можете настроить любое свойство для свойства ключа с помощью ; HasKeyдополнительные сведения см . в общей документации EF по ключам .
Разработчики, поступающие в Azure Cosmos DB из других баз данных, иногда ожидают, что свойство ключа (Id
) создается автоматически. Например, в SQL Server EF настраивает числовые свойства ключа для столбцов IDENTITY, в которых автоматически добавляются значения в базе данных. В отличие от этого, Azure Cosmos DB не поддерживает автоматическое создание свойств, поэтому ключевые свойства должны быть явно заданы. Вставка типа сущности с свойством ключа без набора просто вставляет значение по умолчанию CLR для этого свойства (например, 0 для int
), а вторая вставка завершится ошибкой; EF выдает предупреждение при попытке сделать это.
Если вы хотите иметь GUID в качестве свойства ключа, можно настроить EF для создания уникальных случайных значений на клиенте:
modelBuilder.Entity<Session>().Property(b => b.Id).HasValueGenerator<GuidValueGenerator>();
Ключи секции
Azure Cosmos DB использует секционирование для горизонтального масштабирования; правильное моделирование и тщательный выбор ключа секции жизненно важен для достижения хорошей производительности и снижения затрат. Настоятельно рекомендуется ознакомиться с документацией по секционированию Azure Cosmos DB и заранее спланировать стратегию секционирования.
Чтобы настроить ключ секции с помощью EF, вызовите HasPartitionKey, передавая его регулярное свойство в типе сущности:
modelBuilder.Entity<Order>().HasPartitionKey(o => o.PartitionKey);
Любое свойство можно сделать в ключ секции до тех пор, пока оно преобразуется в строку. После настройки свойство ключа секции всегда должно иметь значение, отличное от NULL; При попытке вставить новый тип сущности с свойством ключа секции без набора приведет к ошибке.
Обратите внимание, что Azure Cosmos DB позволяет двум документам с id
одинаковым свойством существовать в контейнере, если они находятся в разных секциях. Это означает, что для уникальной идентификации документа в id
контейнере все свойства ключа секции должны быть предоставлены. Из-за этого внутреннее представление EF первичного ключа сущности содержит оба этих элемента по соглашению, в отличие от реляционных баз данных, где отсутствует концепция ключа секции. Это означает, что для FindAsync
этого требуются свойства ключа и раздела (см. дополнительные документы), а запрос должен указать их в предложении Where
, чтобы получить выгоду от эффективной и экономичной point reads
.
Обратите внимание, что ключ секции определен на уровне контейнера. В частности, это означает, что для нескольких типов сущностей в одном контейнере невозможно иметь разные свойства ключа секции. Если необходимо определить разные ключи секций, сопоставить соответствующие типы сущностей с различными контейнерами.
Ключи иерархических разделов
Azure Cosmos DB также поддерживает иерархические ключи секций для оптимизации распределения данных; Дополнительные сведения см. в документации. EF 9.0 добавил поддержку иерархических ключей секций; Чтобы настроить эти параметры, просто передайте до 3 свойств в HasPartitionKey:
modelBuilder.Entity<Order>().HasPartitionKey(o => new { e.TenantId, e.UserId, e.SessionId });
С таким иерархическим ключом секции запросы можно легко отправлять только в соответствующее подмножество подразделов. Например, если вы запрашиваете заказы определенного клиента, эти запросы будут выполняться только в вложенных секциях для этого клиента.
Если ключ секции не настроен в EF, при запуске будет зарегистрировано предупреждение; EF Core создаст контейнеры с набором __partitionKey
ключей секции и не будет предоставлять для него никакое значение при вставке элементов. Если ключ секции не задан, контейнер будет ограничен 20 ГБ данных, что является максимальным хранилищем для одной логической секции. Хотя это может работать для небольших приложений разработки и тестирования, настоятельно не рекомендуется развертывать рабочее приложение без хорошо настроенной стратегии ключа секции.
После правильной настройки свойств ключа секции можно указать значения в запросах; Дополнительные сведения см. в разделе "Запрос с помощью ключей секций".
Дискриминаторы
Так как несколько типов сущностей могут быть сопоставлены с тем же контейнером $type
, EF Core всегда добавляет дискриминационное свойство ко всем сохраненным документам JSON (это свойство было вызвано Discriminator
до EF 9.0). Это позволяет EF распознавать документы, загруженные из базы данных, и материализовать правильный тип .NET. Разработчики, поступающие из реляционных баз данных, могут быть знакомы с дискриминационными в контексте наследования таблиц на иерархию (TPH); в Azure Cosmos DB дискриминаторы используются не только в сценариях сопоставления наследования, но и из-за того, что один контейнер может содержать совершенно разные типы документов.
Имя и значения дискриминационных свойств можно настроить с помощью стандартных API EF, дополнительные сведения см. в этих документах. Если вы сопоставляете один тип сущности с контейнером, убедитесь, что вы никогда не будете сопоставлять друг друга и хотите избавиться от дискриминационных свойств, вызовите HasNoDiscriminator:
modelBuilder.Entity<Order>().HasNoDiscriminator();
Так как один и тот же контейнер может содержать разные типы сущностей, а свойство JSON id
должно быть уникальным в пределах секции контейнера, вы не можете иметь одинаковое id
значение для сущностей разных типов в одной секции контейнера. Сравните это с реляционными базами данных, где каждый тип сущности сопоставляется с другой таблицей и поэтому имеет собственное, отдельное пространство ключей. Поэтому вы несете ответственность за id
уникальность документов, которые вы вставляете в контейнер. Если необходимо иметь разные типы сущностей с одинаковыми значениями первичного ключа, вы можете указать EF автоматически вставить дискриминатор в id
свойство следующим образом:
modelBuilder.Entity<Session>().HasDiscriminatorInJsonId();
Хотя это может упростить работу со значениями, это может сделать его более сложным для взаимодействия с внешними приложениями, работающими с id
документами, так как теперь они должны учитывать объединенный формат EF id
, а также дискриминационные значения, которые по умолчанию являются производными от типов .NET. Обратите внимание, что это поведение по умолчанию до EF 9.0.
Дополнительным вариантом является указание EF вставить только корневой дискриминатор, который является дискриминационным типом корневой сущности иерархии, в id
свойство:
modelBuilder.Entity<Session>().HasRootDiscriminatorInJsonId();
Это аналогично, но позволяет EF использовать эффективные операции чтения точек в дополнительных сценариях. Если необходимо вставить дискриминацию в id
собственность, рекомендуется вставить корневой дискриминатор для повышения производительности.
Подготовленная пропускная способность
Если вы используете EF Core для создания базы данных Или контейнеров Azure Cosmos DB, можно настроить подготовленную пропускную способность для базы данных путем вызова CosmosModelBuilderExtensions.HasAutoscaleThroughput или CosmosModelBuilderExtensions.HasManualThroughput. Например:
modelBuilder.HasManualThroughput(2000);
modelBuilder.HasAutoscaleThroughput(4000);
Чтобы настроить подготовленную пропускную способность для контейнера, вызовите CosmosEntityTypeBuilderExtensions.HasAutoscaleThroughput или CosmosEntityTypeBuilderExtensions.HasManualThroughput. Например:
modelBuilder.Entity<Family>(
entityTypeBuilder =>
{
entityTypeBuilder.HasManualThroughput(5000);
entityTypeBuilder.HasAutoscaleThroughput(3000);
});
Срок жизни
Типы сущностей в модели Azure Cosmos DB можно настроить с использованием времени жизни по умолчанию. Например:
modelBuilder.Entity<Hamlet>().HasDefaultTimeToLive(3600);
Или для аналитического хранилища:
modelBuilder.Entity<Hamlet>().HasAnalyticalStoreTimeToLive(3600);
Время жизни для отдельных сущностей можно задать с помощью свойства, сопоставленного с ttl в документе JSON. Рассмотрим пример.
modelBuilder.Entity<Village>()
.HasDefaultTimeToLive(3600)
.Property(e => e.TimeToLive)
.ToJsonProperty("ttl");
Примечание.
Время жизни по умолчанию должно быть настроено в типе сущности для значения ttl. Дополнительные сведения см. в статье "Время жизни" (TTL) в Azure Cosmos DB.
Затем свойство времени в реальном времени задается перед сохранением сущности. Например:
var village = new Village { Id = "DN41", Name = "Healing", TimeToLive = 60 };
context.Add(village);
await context.SaveChangesAsync();
Свойство time-to-live может быть теневым свойством, чтобы избежать загрязнения сущности домена с проблемами базы данных. Например:
modelBuilder.Entity<Hamlet>()
.HasDefaultTimeToLive(3600)
.Property<int>("TimeToLive")
.ToJsonProperty("ttl");
Затем свойство теневого времени в реальном времени задается путем доступа к отслеживаемой сущности. Например:
var hamlet = new Hamlet { Id = "DN37", Name = "Irby" };
context.Add(hamlet);
context.Entry(hamlet).Property("TimeToLive").CurrentValue = 60;
await context.SaveChangesAsync();
Внедренные сущности
Примечание.
Связанные типы сущностей настроены как принадлежащие по умолчанию. Чтобы изменить это для конкретного типа сущности, вызовите ModelBuilder.Entity.
Для Azure Cosmos DB принадлежащие сущности внедряются в тот же элемент, что и владелец. Изменить имя свойства можно с помощью 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");
});
В такой конфигурации порядок из приведенного выше примера сохраняется так:
{
"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
}
Также внедряются коллекции зависимых сущностей. В следующем примере мы будем использовать класс Distributor
с коллекцией StreetAddress
:
public class Distributor
{
public int Id { get; set; }
public string ETag { get; set; }
public ICollection<StreetAddress> ShippingCenters { get; set; }
}
Зависимым сущностям не нужно предоставлять значения ключа явным образом для хранения:
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();
}
Они будут сохранены так:
{
"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 всегда должны присутствовать уникальные значения ключа для всех отслеживаемых сущностей. Первичный ключ, создаваемый по умолчанию для коллекций зависимых типов, состоит из свойств внешнего ключа, указывающих на владельца, и свойства int
, соответствующего индексу в массиве JSON. Чтобы получить запись этих значений, можно использовать 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();
}
Совет
При необходимости можно изменить первичный ключ по умолчанию для принадлежащих типов сущностей, а затем указать значения ключа явным образом.
Коллекции примитивных типов
Обнаружение и сопоставление коллекций поддерживаемых примитивных типов, таких как string
и int
, происходит автоматически. Все типы, реализующие IReadOnlyList<T> или IReadOnlyDictionary<TKey,TValue>, являются поддерживаемыми коллекциями. Например, рассмотрим следующий тип сущности:
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; }
}
IDictionary
Их IList
можно заполнить и сохранить в базе данных:
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();
В результате будет получен следующий документ 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
}
Затем эти коллекции можно обновить обычным способом:
book.Quotes.Add("Pressing the emergency button lowered the rods again.");
book.Notes["48"] = "Chiesa d'Oro";
await context.SaveChangesAsync();
Ограничения:
- Поддерживаются только словари со строковыми ключами.
- Добавлена поддержка запросов к примитивным коллекциям в EF Core 9.0.
Оптимистический параллелизм с тегами ETag
Чтобы настроить тип сущности для использования оптимистического параллелизма, вызовите UseETagConcurrency. Этот вызов создает свойство _etag
в теневом состоянии и устанавливает его в качестве маркера параллелизма.
modelBuilder.Entity<Order>()
.UseETagConcurrency();
Чтобы упростить устранение ошибок параллелизма, можно сопоставить eTag со свойством CLR, используя IsETagConcurrency.
modelBuilder.Entity<Distributor>()
.Property(d => d.ETag)
.IsETagConcurrency();