Дополнительные функции Отслеживание изменений
В этом документе рассматриваются другие функции и сценарии, связанные с отслеживанием изменений.
Совет
В этом документе предполагается, что состояния сущности и основы отслеживания изменений EF Core понятны. Дополнительные сведения об этих разделах см. в Отслеживание изменений в EF Core.
Совет
Вы можете запустить и отладить весь код, используемый в этой документации, скачав пример кода из GitHub.
Add
и AddAsync
Entity Framework Core (EF Core) предоставляет асинхронные методы при использовании этого метода, что может привести к взаимодействию с базой данных. Синхронные методы также предоставляются для предотвращения накладных расходов при использовании баз данных, которые не поддерживают высокопроизводительный асинхронный доступ.
DbContext.Add и DbSet<TEntity>.Add обычно не обращаются к базе данных, так как эти методы по сути просто запускают сущности отслеживания. Однако некоторые формы создания значений могут получить доступ к базе данных, чтобы создать значение ключа. Единственный генератор значений, который делает это и поставляется HiLoValueGenerator<TValue>с EF Core. Использование этого генератора является редким; Он никогда не настраивается по умолчанию. Это означает, что подавляющее большинство приложений должно использовать Add
, а не AddAsync
.
Другие аналогичные методы, такие как Update
, Attach
и Remove
не имеют асинхронных перегрузок, так как они никогда не создают новые значения ключей, поэтому никогда не требуется обращаться к базе данных.
AddRange
, UpdateRange
, AttachRange
и RemoveRange
DbSet<TEntity>и DbContext укажите альтернативные версии , Update
Attach
и Remove
принимающее несколько экземпляров Add
в одном вызове. Эти методы: AddRange, AttachRangeUpdateRangeи RemoveRange соответственно.
Эти методы предоставляются в качестве удобства. Использование метода range имеет ту же функциональность, что и несколько вызовов эквивалентного метода, отличного от диапазона. Между двумя подходами нет существенной разницы в производительности.
Примечание.
Это отличается от EF6, где AddRange
и Add
оба автоматически вызываются DetectChanges
, но вызов Add
несколько раз приводил к вызову DetectChanges несколько раз, а не один раз. Это сделало AddRange
более эффективным в EF6. В EF Core ни из этих методов не вызывается DetectChanges
автоматически.
Методы DbContext и DbSet
Многие методы, в том числе Add
, Update
Attach
и Remove
, имеют реализации для обоих DbSet<TEntity> и DbContext. Эти методы имеют точно то же поведение для обычных типов сущностей. Это связано с тем, что тип СРЕДЫ CLR сущности сопоставляется с одним и только одним типом сущности в модели EF Core. Таким образом, тип CLR полностью определяет, где сущность соответствует модели, и поэтому dbSet для использования можно определить неявно.
Исключением из этого правила является использование типов сущностей общего типа, которые в основном используются для сущностей соединения "многие ко многим". При использовании типа сущности общего типа необходимо сначала создать DbSet для используемого типа модели EF Core. Такие методы, как Add
, Update
Attach
и Remove
затем можно использовать в DbSet без неоднозначности относительно того, какой тип модели EF Core используется.
Типы сущностей общего типа используются по умолчанию для сущностей соединения в отношениях "многие ко многим". Тип сущности общего типа также можно явно настроить для использования в связи "многие ко многим". Например, приведенный ниже код настраивается Dictionary<string, int>
как тип сущности соединения:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder
.SharedTypeEntity<Dictionary<string, int>>(
"PostTag",
b =>
{
b.IndexerProperty<int>("TagId");
b.IndexerProperty<int>("PostId");
});
modelBuilder.Entity<Post>()
.HasMany(p => p.Tags)
.WithMany(p => p.Posts)
.UsingEntity<Dictionary<string, int>>(
"PostTag",
j => j.HasOne<Tag>().WithMany(),
j => j.HasOne<Post>().WithMany());
}
Изменение внешних ключей и навигаций показывает, как связать две сущности путем отслеживания нового экземпляра сущности соединения. Приведенный ниже код делает это для типа сущности общего типа, используемого для Dictionary<string, int>
сущности соединения:
using var context = new BlogsContext();
var post = await context.Posts.SingleAsync(e => e.Id == 3);
var tag = await context.Tags.SingleAsync(e => e.Id == 1);
var joinEntitySet = context.Set<Dictionary<string, int>>("PostTag");
var joinEntity = new Dictionary<string, int> { ["PostId"] = post.Id, ["TagId"] = tag.Id };
joinEntitySet.Add(joinEntity);
Console.WriteLine(context.ChangeTracker.DebugView.LongView);
await context.SaveChangesAsync();
Обратите внимание, что DbContext.Set<TEntity>(String) используется для создания DbSet для типа сущности PostTag
. Затем этот dbSet можно использовать для вызова Add
с новым экземпляром сущности соединения.
Важно!
Тип СРЕДЫ CLR, используемый для типов сущностей соединения по соглашению, может измениться в будущих выпусках, чтобы повысить производительность. Не зависят от какого-либо конкретного типа сущности соединения, если оно не было явно настроено, как показано Dictionary<string, int>
в приведенном выше коде.
Доступ к свойствам и полям
Доступ к свойствам сущности использует резервное поле свойства по умолчанию. Это эффективно и избегает активации побочных эффектов от вызова методов получения свойств и наборов. Например, это то, как отложенная загрузка может избежать активации бесконечных циклов. Дополнительные сведения о настройке полей резервной копии в модели см. в разделе "Резервные поля ".
Иногда может потребоваться для EF Core создавать побочные эффекты при изменении значений свойств. Например, когда привязка данных к сущностям, установка свойства может создавать уведомления в U.I. которые не происходят при настройке поля напрямую. Это можно сделать, изменив следующее PropertyAccessMode :
- Все типы сущностей в модели с помощью ModelBuilder.UsePropertyAccessMode
- Все свойства и навигации определенного типа сущности с помощью EntityTypeBuilder<TEntity>.UsePropertyAccessMode
- Определенное свойство с помощью PropertyBuilder.UsePropertyAccessMode
- Определенная навигация с помощью NavigationBuilder.UsePropertyAccessMode
Режимы Field
доступа к свойствам и PreferField
приведет к тому, что EF Core получит доступ к значению свойства через его резервное поле. Аналогичным образом и PreferProperty
приведет к тому, Property
что EF Core получит доступ к значению свойства через метод получения и задания.
Если Field
или используется, Property
ef Core не может получить доступ к значению через поле или свойство getter/setter соответственно, EF Core вызовет исключение. Это гарантирует, что EF Core всегда использует доступ к полю или свойству при его использовании.
С другой стороны, режимы и PreferProperty
режимы будут возвращаться к использованию свойства или резервного поля соответственно, PreferField
если невозможно использовать предпочтительный доступ. Значение по умолчанию — PreferField
. Это означает, что EF Core будет использовать поля всякий раз, но не завершится ошибкой, если к свойству необходимо получить доступ через метод получения или задания.
FieldDuringConstruction
и PreferFieldDuringConstruction
настройте EF Core для использования полей резервного копирования только при создании экземпляров сущностей. Это позволяет выполнять запросы без побочных эффектов получения и задания, а последующие изменения свойств EF Core вызывают эти побочные эффекты.
Различные режимы доступа к свойствам приведены в следующей таблице:
PropertyAccessMode | Предпочтение | Выбор создания сущностей | Резерв | Резервное создание сущностей |
---|---|---|---|---|
Field |
Поле | Поле | Активизирует исключение | Активизирует исключение |
Property |
Свойство | Свойство | Активизирует исключение | Активизирует исключение |
PreferField |
Поле | Поле | Свойство | Свойство |
PreferProperty |
Свойство | Свойство | Поле | Поле |
FieldDuringConstruction |
Свойство | Поле | Поле | Активизирует исключение |
PreferFieldDuringConstruction |
Свойство | Поле | Поле | Свойство |
Временные значения
EF Core создает временные значения ключей при отслеживании новых сущностей, которые будут иметь реальные значения ключей, созданные базой данных при вызове SaveChanges. Сведения об использовании временных значений см. в Отслеживание изменений в EF Core.
Доступ к временным значениям
Временные значения хранятся в отслеживании изменений и не устанавливаются непосредственно на экземпляры сущностей. Однако эти временные значения предоставляются при использовании различных механизмов для доступа к отслеживаемых сущностям. Например, следующий код обращается к временному значению с помощью EntityEntry.CurrentValues:
using var context = new BlogsContext();
var blog = new Blog { Name = ".NET Blog" };
context.Add(blog);
Console.WriteLine($"Blog.Id set on entity is {blog.Id}");
Console.WriteLine($"Blog.Id tracked by EF is {context.Entry(blog).Property(e => e.Id).CurrentValue}");
Выходные данные из этого кода:
Blog.Id set on entity is 0
Blog.Id tracked by EF is -2147482643
PropertyEntry.IsTemporaryможно использовать для проверка временных значений.
Управление временными значениями
Иногда полезно явно работать с временными значениями. Например, коллекцию новых сущностей можно создать на веб-клиенте, а затем сериализовать обратно на сервер. Значения внешнего ключа — один из способов настройки связей между этими сущностями. Следующий код использует этот подход для связывания графа новых сущностей по внешнему ключу, при этом при вызове SaveChanges создаются реальные значения ключей.
var blogs = new List<Blog> { new Blog { Id = -1, Name = ".NET Blog" }, new Blog { Id = -2, Name = "Visual Studio Blog" } };
var posts = new List<Post>
{
new Post
{
Id = -1,
BlogId = -1,
Title = "Announcing the Release of EF Core 5.0",
Content = "Announcing the release of EF Core 5.0, a full featured cross-platform..."
},
new Post
{
Id = -2,
BlogId = -2,
Title = "Disassembly improvements for optimized managed debugging",
Content = "If you are focused on squeezing out the last bits of performance for your .NET service or..."
}
};
using var context = new BlogsContext();
foreach (var blog in blogs)
{
context.Add(blog).Property(e => e.Id).IsTemporary = true;
}
foreach (var post in posts)
{
context.Add(post).Property(e => e.Id).IsTemporary = true;
}
Console.WriteLine(context.ChangeTracker.DebugView.LongView);
await context.SaveChangesAsync();
Console.WriteLine(context.ChangeTracker.DebugView.LongView);
Обратите внимание на указанные ниже моменты.
- Отрицательные числа используются в качестве временных значений ключей; это не обязательно, но является общим соглашением, чтобы предотвратить ключевые столкновения.
- Свойство
Post.BlogId
FK назначается тем же отрицательным значением, что и PK связанного блога. - Значения PK помечаются как временные по параметру IsTemporary после отслеживания каждой сущности. Это необходимо, так как любое ключевое значение, предоставленное приложением, считается реальным значением ключа.
Просмотр представления отладки отслеживания изменений перед вызовом SaveChanges показывает, что значения PK помечены как временные и записи связаны с правильными блогами, включая исправление навигаций:
Blog {Id: -2} Added
Id: -2 PK Temporary
Name: 'Visual Studio Blog'
Posts: [{Id: -2}]
Blog {Id: -1} Added
Id: -1 PK Temporary
Name: '.NET Blog'
Posts: [{Id: -1}]
Post {Id: -2} Added
Id: -2 PK Temporary
BlogId: -2 FK
Content: 'If you are focused on squeezing out the last bits of perform...'
Title: 'Disassembly improvements for optimized managed debugging'
Blog: {Id: -2}
Tags: []
Post {Id: -1} Added
Id: -1 PK Temporary
BlogId: -1 FK
Content: 'Announcing the release of EF Core 5.0, a full featured cross...'
Title: 'Announcing the Release of EF Core 5.0'
Blog: {Id: -1}
После вызова SaveChangesэти временные значения были заменены реальными значениями, созданными базой данных:
Blog {Id: 1} Unchanged
Id: 1 PK
Name: '.NET Blog'
Posts: [{Id: 1}]
Blog {Id: 2} Unchanged
Id: 2 PK
Name: 'Visual Studio Blog'
Posts: [{Id: 2}]
Post {Id: 1} Unchanged
Id: 1 PK
BlogId: 1 FK
Content: 'Announcing the release of EF Core 5.0, a full featured cross...'
Title: 'Announcing the Release of EF Core 5.0'
Blog: {Id: 1}
Tags: []
Post {Id: 2} Unchanged
Id: 2 PK
BlogId: 2 FK
Content: 'If you are focused on squeezing out the last bits of perform...'
Title: 'Disassembly improvements for optimized managed debugging'
Blog: {Id: 2}
Tags: []
Работа со значениями по умолчанию
EF Core позволяет свойству получить значение по умолчанию из базы данных при SaveChanges вызове. Как и в случае с созданными значениями ключей, EF Core будет использовать только значение по умолчанию из базы данных, если значение не было явно задано. Например, рассмотрим следующий тип сущности:
public class Token
{
public int Id { get; set; }
public string Name { get; set; }
public DateTime ValidFrom { get; set; }
}
Свойство ValidFrom
настроено для получения значения по умолчанию из базы данных:
modelBuilder
.Entity<Token>()
.Property(e => e.ValidFrom)
.HasDefaultValueSql("CURRENT_TIMESTAMP");
При вставке сущности этого типа EF Core позволит базе данных создать значение, если вместо этого не задано явное значение. Например:
using var context = new BlogsContext();
context.AddRange(
new Token { Name = "A" },
new Token { Name = "B", ValidFrom = new DateTime(1111, 11, 11, 11, 11, 11) });
await context.SaveChangesAsync();
Console.WriteLine(context.ChangeTracker.DebugView.LongView);
В представлении отладки средства отслеживания изменений показано, что первый маркер, ValidFrom
созданный базой данных, а второй маркер использовал значение явно задано:
Token {Id: 1} Unchanged
Id: 1 PK
Name: 'A'
ValidFrom: '12/30/2020 6:36:06 PM'
Token {Id: 2} Unchanged
Id: 2 PK
Name: 'B'
ValidFrom: '11/11/1111 11:11:11 AM'
Примечание.
Для использования значений базы данных по умолчанию требуется, чтобы столбец базы данных был настроен на ограничение значения по умолчанию. Это выполняется автоматически миграцией EF Core при использовании HasDefaultValueSql или HasDefaultValue. Не забудьте создать ограничение по умолчанию для столбца другим способом, если миграция EF Core не используется.
Использование свойств, допускающих значение NULL
EF Core может определить, задано ли свойство или нет, сравнивая значение свойства со значением среды CLR по умолчанию для этого типа. Это хорошо работает в большинстве случаев, но означает, что среда CLR по умолчанию не может быть явно вставлена в базу данных. Например, рассмотрим сущность со свойством целочисленного числа:
public class Foo1
{
public int Id { get; set; }
public int Count { get; set; }
}
Где это свойство настроено для базы данных по умолчанию –1:
modelBuilder
.Entity<Foo1>()
.Property(e => e.Count)
.HasDefaultValue(-1);
Цель заключается в том, что значение по умолчанию -1 будет использоваться всякий раз, когда явное значение не задано. Однако если значение равно 0 (значение CLR по умолчанию для целых чисел) неизменяемо для EF Core, то это означает, что невозможно вставить значение 0 для этого свойства. Например:
using var context = new BlogsContext();
var fooA = new Foo1 { Count = 10 };
var fooB = new Foo1 { Count = 0 };
var fooC = new Foo1();
context.AddRange(fooA, fooB, fooC);
await context.SaveChangesAsync();
Debug.Assert(fooA.Count == 10);
Debug.Assert(fooB.Count == -1); // Not what we want!
Debug.Assert(fooC.Count == -1);
Обратите внимание, что экземпляр, в котором Count
явно задано значение 0, по-прежнему получает значение по умолчанию из базы данных, что не является тем, что мы намеревались. Простой способ справиться с этим заключается в том, чтобы сделать Count
свойство nullable:
public class Foo2
{
public int Id { get; set; }
public int? Count { get; set; }
}
Это делает значение CLR по умолчанию null вместо 0, что означает, что 0 теперь будет вставлено при явном установке:
using var context = new BlogsContext();
var fooA = new Foo2 { Count = 10 };
var fooB = new Foo2 { Count = 0 };
var fooC = new Foo2();
context.AddRange(fooA, fooB, fooC);
await context.SaveChangesAsync();
Debug.Assert(fooA.Count == 10);
Debug.Assert(fooB.Count == 0);
Debug.Assert(fooC.Count == -1);
Использование полей резервной копии, допускающих значение NULL
Проблема, связанная с тем, что свойство имеет значение NULL, которое может не быть концептуально пустым в модели домена. Поэтому принудительное применение свойства к значению NULL компрометирует модель.
Свойство может быть оставлено не допускающим значение NULL, причем только поле резервного копирования является пустым. Например:
public class Foo3
{
public int Id { get; set; }
private int? _count;
public int Count
{
get => _count ?? -1;
set => _count = value;
}
}
Это позволяет вставить значение по умолчанию CLR (0), если свойство явно имеет значение 0, а не требуется предоставлять свойство в качестве значения NULL в модели домена. Например:
using var context = new BlogsContext();
var fooA = new Foo3 { Count = 10 };
var fooB = new Foo3 { Count = 0 };
var fooC = new Foo3();
context.AddRange(fooA, fooB, fooC);
await context.SaveChangesAsync();
Debug.Assert(fooA.Count == 10);
Debug.Assert(fooB.Count == 0);
Debug.Assert(fooC.Count == -1);
Поля резервной копии, допускающие значение NULL для логических свойств
Этот шаблон особенно полезен при использовании логических свойств с созданными магазином значениями по умолчанию. Так как значение по умолчанию clR bool
имеет значение false, это означает, что "false" нельзя вставить явно с помощью обычного шаблона. Например, рассмотрим тип сущности User
:
public class User
{
public int Id { get; set; }
public string Name { get; set; }
private bool? _isAuthorized;
public bool IsAuthorized
{
get => _isAuthorized ?? true;
set => _isAuthorized = value;
}
}
Свойство IsAuthorized
настроено со значением по умолчанию базы данных true:
modelBuilder
.Entity<User>()
.Property(e => e.IsAuthorized)
.HasDefaultValue(true);
Свойство IsAuthorized
можно задать явным образом "true" или "false" перед вставкой, или можно оставить неустановленным, в этом случае будет использоваться база данных по умолчанию:
using var context = new BlogsContext();
var userA = new User { Name = "Mac" };
var userB = new User { Name = "Alice", IsAuthorized = true };
var userC = new User { Name = "Baxter", IsAuthorized = false }; // Always deny Baxter access!
context.AddRange(userA, userB, userC);
await context.SaveChangesAsync();
Выходные данные SaveChanges при использовании SQLite показывают, что база данных по умолчанию используется для Mac, а явные значения задаются для Alice и Baxter:
-- Executed DbCommand (0ms) [Parameters=[@p0='Mac' (Size = 3)], CommandType='Text', CommandTimeout='30']
INSERT INTO "User" ("Name")
VALUES (@p0);
SELECT "Id", "IsAuthorized"
FROM "User"
WHERE changes() = 1 AND "rowid" = last_insert_rowid();
-- Executed DbCommand (0ms) [Parameters=[@p0='True' (DbType = String), @p1='Alice' (Size = 5)], CommandType='Text', CommandTimeout='30']
INSERT INTO "User" ("IsAuthorized", "Name")
VALUES (@p0, @p1);
SELECT "Id"
FROM "User"
WHERE changes() = 1 AND "rowid" = last_insert_rowid();
-- Executed DbCommand (0ms) [Parameters=[@p0='False' (DbType = String), @p1='Baxter' (Size = 6)], CommandType='Text', CommandTimeout='30']
INSERT INTO "User" ("IsAuthorized", "Name")
VALUES (@p0, @p1);
SELECT "Id"
FROM "User"
WHERE changes() = 1 AND "rowid" = last_insert_rowid();
Значение по умолчанию схемы используется только
Иногда полезно использовать значения по умолчанию в схеме базы данных, созданной миграцией EF Core без EF Core, когда-либо используя эти значения для вставок. Это можно сделать, настроив свойство как PropertyBuilder.ValueGeneratedNever например:
modelBuilder
.Entity<Bar>()
.Property(e => e.Count)
.HasDefaultValue(-1)
.ValueGeneratedNever();