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

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

Совет

В этом документе предполагается, что состояния сущностей и основы отслеживания изменений 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 , Update , Attach и Remove , которые принимают несколько экземпляров в одном вызове. Эти методы —,, AddRangeUpdateRangeAttachRange и 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 может быть определен неявно.

Исключением из этого правила является использование типов сущностей с общим типом, которые были введены в EF Core 5,0, в первую очередь для сущностей объединения «многие ко многим». При использовании типа сущности общего типа необходимо сначала создать DbSet для используемого типа модели EF Core. Такие методы Add , как,, Update и, AttachRemove можно использовать в DbSet без какой-либо неоднозначности, в которой используется тип модели EF Core.

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

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

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

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 с новым экземпляром сущности JOIN.

Важно!

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

Сравнение свойств и доступа к полям

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

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

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

Если FieldProperty используются значения или, а EF Core не может получить доступ к значению через поле или метод считывания/задания свойства соответственно, EF Core выдаст исключение. Это гарантирует, что EF Core всегда использует доступ к полям и свойствам, если вы считаете это.

С другой стороны, PreferFieldPreferProperty режимы и будут возвращаться к использованию свойства или резервного поля соответственно, если невозможно использовать предпочтительный доступ. Значение по умолчанию — PreferField. Это означает, что EF Core будет использовать поля каждый раз, когда это возможно, но не завершится ошибкой, если вместо этого доступ к свойству должен осуществляться через метод получения или задания.

FieldDuringConstruction и PreferFieldDuringConstruction настройте EF Core для использования резервных полей FieldDuringConstruction. Это позволяет выполнять запросы без побочных эффектов getter и Set, в то время как изменения свойств по EF Core приведут к этим побочным эффектам.

В следующей таблице приведены различные режимы доступа к свойствам.

пропертякцессмоде Предпочтение Предпочтение созданию сущностей Резервирование Резервное Создание сущностей
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 связанного блога.
  • Значения ПЕРВИЧного ключа помечаются как временные, IsTemporary после того как каждая сущность будет отслеживанием. Это необходимо, так как любое значение ключа, предоставляемое приложением, считается реальным значением ключа.

Просмотр представления отладки средства записи изменений перед вызовом SaveChanges показывает, что значения первичного ключа помечены как временные, а записи связаны с правильными блогами, включая исправление переходов:

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

Просмотр представления отладки "средство записи изменений " показывает, что первый маркер был создан базой данных, тогда как второй токен использовал явно заданный параметр:

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 по умолчанию не может быть явно вставлено в базу данных. Например, рассмотрим сущность со свойством Integer:

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

Этот шаблон особенно полезен при использовании свойств 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();