Дополнительные функции Отслеживание изменений

В этом документе рассматриваются различные функции и сценарии, связанные с отслеживанием изменений.

Совет

В этом документе предполагается, что состояния сущностей и основы отслеживания изменений EF Core понятны. Дополнительные сведения об этих разделах см. в Отслеживание изменений в EF Core.

Совет

Вы можете запустить и отладить весь код, используемый в этой документации, скачав пример кода из GitHub.

Add и AddAsync

Entity Framework Core (EF Core) предоставляет асинхронные методы при использовании этого метода, что может привести к взаимодействию с базой данных. Синхронные методы также предоставляются для предотвращения накладных расходов при использовании баз данных, которые не поддерживают высокопроизводительный асинхронный доступ.

DbContext.Add и DbSet<TEntity>.Add обычно не обращаются к базе данных, так как эти методы изначально просто начинают отслеживать сущности. Однако некоторые формы создания значений могут получить доступ к базе данных, чтобы создать значение ключа. Единственным генератором значений, который делает это и поставляется с EF Core, является HiLoValueGenerator<TValue>. Использование этого генератора является необычным; Он никогда не настраивается по умолчанию. Это означает, что подавляющее большинство приложений должно использовать Add, а не AddAsync.

Другие аналогичные методы, такие как Update, Attachи Remove не имеют асинхронных перегрузок, так как они никогда не создают новые значения ключей и поэтому никогда не должны обращаться к базе данных.

AddRange, UpdateRange, AttachRange и RemoveRange

DbSet<TEntity>и DbContext предоставьте альтернативные Addверсии , AttachUpdateи Remove которые принимают несколько экземпляров в одном вызове. Эти методы: AddRange, UpdateRangeи AttachRangeRemoveRange соответственно.

Эти методы предоставляются в качестве удобства. Использование метода range имеет те же функциональные возможности, что и несколько вызовов эквивалентного метода, отличного от диапазона. Между двумя подходами нет существенной разницы в производительности.

Примечание

Это отличается от EF6, где AddRange и Add оба автоматически вызываются DetectChanges, но вызов Add несколько раз приводил к вызову DetectChanges несколько раз, а не один раз. Это сделало AddRange более эффективным в EF6. В EF Core ни ни из этих методов не вызываются DetectChangesавтоматически.

Методы DbContext и DbSet

Многие методы, в том числе Add, UpdateAttachи Remove, имеют реализации для обоих DbSet<TEntity> и DbContext. Эти методы имеют точно такое же поведение для обычных типов сущностей. Это связано с тем, что тип СРЕДЫ CLR сущности сопоставляется с одним и только одним типом сущности в модели EF Core. Таким образом, тип СРЕДЫ CLR полностью определяет, где сущность помещается в модель, и поэтому используемый dbSet можно определить неявно.

Исключением из этого правила является использование типов сущностей общего типа, появившихся в EF Core 5.0, в основном для сущностей соединения "многие ко многим". При использовании типа сущности общего типа необходимо сначала создать DbSet для используемого типа модели EF Core. Такие методы, как Add, UpdateAttachи 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 = context.Posts.Single(e => e.Id == 3);
var tag = context.Tags.Single(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);

context.SaveChanges();

Обратите внимание, что DbContext.Set<TEntity>(String) используется для создания DbSet для типа сущности PostTag . Затем этот dbSet можно использовать для вызова Add с новым экземпляром сущности соединения.

Важно!

Тип CLR, используемый для типов сущностей соединения по соглашению, может измениться в будущих выпусках, чтобы повысить производительность. Не зависит от какого-либо конкретного типа сущности соединения, если он не был явно настроен, как показано Dictionary<string, int> в приведенном выше коде.

Доступ к свойствам и полям

Доступ к свойствам сущности по умолчанию использует резервное поле свойства. Это эффективно и позволяет избежать активации побочных эффектов при вызове методов получения свойств и методов задания. Например, это способ отложенной загрузки позволяет избежать активации бесконечных циклов. Дополнительные сведения о настройке резервных полей в модели см. в разделе "Резервные поля ".

Иногда может потребоваться для EF Core создавать побочные эффекты при изменении значений свойств. Например, если привязка данных к сущностям, установка свойства может создавать уведомления для U.I. Которые не происходят при настройке поля напрямую. Это можно достичь, изменив PropertyAccessMode следующие возможности:

Режимы Field доступа к свойствам и PreferField помогут EF Core получить доступ к значению свойства через его резервное поле. Аналогичным образом и PreferProperty приведет к тому, Property что EF Core получит доступ к значению свойства через метод получения и задания.

Если Field или Property используется, а EF Core не может получить доступ к значению через поле или метод получения или задания свойства соответственно, 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.

Доступ к временным значениям

Начиная с EF Core 3.0 временные значения хранятся в отслеживании изменений и не устанавливаются непосредственно на экземпляры сущностей. Однако эти временные значения предоставляются при использовании различных механизмов доступа к отслеживаемых сущностям. Например, следующий код обращается к временному значению с помощью 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);

context.SaveChanges();

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

context.SaveChanges();

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

Debug.Assert(fooA.Count == 10);
Debug.Assert(fooB.Count == -1); // Not what we want!
Debug.Assert(fooC.Count == -1);

Обратите внимание, что экземпляр, для которого Count явно задано значение 0, по-прежнему получает значение по умолчанию из базы данных, которое не является предполагаемым. Это простой способ решения заключается в том Count , чтобы сделать свойство допускаемым значение NULL:

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

Debug.Assert(fooA.Count == 10);
Debug.Assert(fooB.Count == 0);
Debug.Assert(fooC.Count == -1);

Использование резервных полей, допускающих значение NULL

Примечание

Этот шаблон резервного поля, допускающего значение NULL, поддерживается EF Core 5.0 и более поздних версий.

Проблема, связанная с тем, что свойство допускает значение NULL, которое может быть не допускается концептуально в модели предметной области. Поэтому принудив свойство быть допускаемым значением NULL, скомпрометирует модель.

Начиная с EF Core 5.0 свойство может быть оставлено не допускающим значения 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);
context.SaveChanges();

Debug.Assert(fooA.Count == 10);
Debug.Assert(fooB.Count == 0);
Debug.Assert(fooC.Count == -1);

Поля резервной копии, допускающие значение NULL, для свойств bool

Этот шаблон особенно полезен при использовании логических свойств с созданными магазином значениями по умолчанию. Так как значение по умолчанию 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);

context.SaveChanges();

Выходные данные SaveChanges при использовании SQLite показывают, что база данных по умолчанию используется для Mac, а для Алисы и Бакстер заданы явные значения:

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