Ядро Entity Framework: самообращающееся значение столбца FK заменяется на `NULL` - PullRequest
0 голосов
/ 10 ноября 2018

Target Framework : .Net Core 2.1

EntityFrameworkCore : 2.1.4

Я столкнулся с некоторым поведением, которое я не понимаю при звонке SaveChanges(). Как видно из заголовка: у меня есть действительные идентификаторы GUID, имеющие отношение внешнего ключа, и при вставке новых записей эта же таблица заменяется на NULL.

Это происходит только для определенного столбца, и только когда EF генерирует SQL для изменений, внесенных в контекст. Я могу вставить те же значения точно в SSMS.

Для демонстрации я создал минимальный репозиторий на GitHub , который вы можете использовать для воссоздания точной проблемы, с которой я сталкиваюсь, но в двух словах, если у вас есть такая сущность:

public class Account
{
    [Key]
    public Guid AccountId { get; set; }

    #region Audit

    public Guid? AddedByAccountId { get; set; }

    public DateTime AddedOnUtc { get; set; }

    public Guid? ModifiedByAccountId { get; set; }

    public DateTime ModifiedOnUtc { get; set; }

    #endregion Audit

    #region Navigation Properties

    [ForeignKey(nameof(AddedByAccountId))]
    public virtual Account AddedByAccount { get; set; }

    [ForeignKey(nameof(ModifiedByAccountId))]
    public virtual Account ModifiedByAccount { get; set; }

    #endregion Navigation Properties
}

И производная реализация DbContext, подобная этой:

public class EntityFrameworkDbContext : DbContext
{
    public EntityFrameworkDbContext(DbContextOptions<EntityFrameworkDbContext> options)
        : base(options)
    {
    }

    public DbSet<Account> Accounts { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Account>(entity =>
        {
            // For some reason this self referencing key generates a unique
            // constraint in the migration script if we don't set this here.

            // "ModifiedByAccountId" does not have this issue! I suspect this might 
            // the root cause of the NULL data issue.
            entity.HasIndex(e => e.AddedByAccountId).IsUnique(false);
        });
    }
}

Вставка чего-либо в контекст должна давать те же результаты. Например:

private void InitialiseDatabase()
{
    var systemAccount = new Account
    {
        AccountId = Guid.Parse("35c38df0-a959-4232-aadd-40db2260f557"),
        AddedByAccountId = null,
        AddedOnUtc = DateTime.UtcNow,
        ModifiedByAccountId = null,
        ModifiedOnUtc = DateTime.UtcNow
    };

    var otherAccounts = new List<Account>
    {
        new Account
        {
            AccountId = Guid.Parse("015b76fc-2833-45d9-85a7-ab1c389c1c11"),
            AddedByAccountId = Guid.Parse("35c38df0-a959-4232-aadd-40db2260f557"),
            AddedOnUtc = DateTime.UtcNow,
            ModifiedByAccountId = Guid.Parse("35c38df0-a959-4232-aadd-40db2260f557"),
            ModifiedOnUtc = DateTime.UtcNow
        },
        new Account
        {
            AccountId = Guid.Parse("538ee0dd-531a-41c6-8414-0769ec5990d8"),
            AddedByAccountId = Guid.Parse("35c38df0-a959-4232-aadd-40db2260f557"),
            AddedOnUtc = DateTime.UtcNow,
            ModifiedByAccountId = Guid.Parse("35c38df0-a959-4232-aadd-40db2260f557"),
            ModifiedOnUtc = DateTime.UtcNow
        },
        new Account
        {
            AccountId = Guid.Parse("8288d9ac-fbce-417e-89ef-82266b284b78"),
            AddedByAccountId = Guid.Parse("35c38df0-a959-4232-aadd-40db2260f557"),
            AddedOnUtc = DateTime.UtcNow,
            ModifiedByAccountId = Guid.Parse("35c38df0-a959-4232-aadd-40db2260f557"),
            ModifiedOnUtc = DateTime.UtcNow
        },
        new Account
        {
            AccountId = Guid.Parse("4bcfe9f8-e4a5-49f0-b6ee-44871632a903"),
            AddedByAccountId = Guid.Parse("35c38df0-a959-4232-aadd-40db2260f557"),
            AddedOnUtc = DateTime.UtcNow,
            ModifiedByAccountId = Guid.Parse("35c38df0-a959-4232-aadd-40db2260f557"),
            ModifiedOnUtc = DateTime.UtcNow
        }
    };

    _dbContext.Add(systemAccount);
    _dbContext.AddRange(otherAccounts);

    try
    {
        _dbContext.SaveChanges();
    }
    catch (Exception ex)
    {
        // Just checking to see if anything was being raised.
    }            
}

Создает следующий SQL (захваченный с использованием SQL Server Profiler и отформатированный для удобства чтения):

exec sp_executesql N'SET NOCOUNT ON;

INSERT INTO [Accounts] 
(
    [AccountId], 
    [AddedByAccountId], 
    [AddedOnUtc], 
    [ModifiedByAccountId], 
    [ModifiedOnUtc]
)
VALUES 
(
    @p5, 
    @p6, 
    @p7, 
    @p8, 
    @p9
),
(
    @p10, 
    @p11, 
    @p12, 
    @p13, 
    @p14
),
(
    @p15, 
    @p16, 
    @p17, 
    @p18, 
    @p19
),
(
    @p20, 
    @p21, 
    @p22, 
    @p23, 
    @p24
);
',N'@p5 uniqueidentifier,
@p6 uniqueidentifier,
@p7 datetime2(7),
@p8 uniqueidentifier,
@p9 datetime2(7),
@p10 uniqueidentifier,
@p11 uniqueidentifier,
@p12 datetime2(7),
@p13 uniqueidentifier,
@p14 datetime2(7),
@p15 uniqueidentifier,
@p16 uniqueidentifier,
@p17 datetime2(7),
@p18 uniqueidentifier,
@p19 datetime2(7),
@p20 uniqueidentifier,
@p21 uniqueidentifier,
@p22 datetime2(7),
@p23 uniqueidentifier,
@p24 datetime2(7)',
@p5='015B76FC-2833-45D9-85A7-AB1C389C1C11',
@p6=NULL,
@p7='2018-11-10 14:29:25.5363017',
@p8='35C38DF0-A959-4232-AADD-40DB2260F557',
@p9='2018-11-10 14:29:25.5363022',
@p10='538EE0DD-531A-41C6-8414-0769EC5990D8',
@p11=NULL,
@p12='2018-11-10 14:29:25.5363031',
@p13='35C38DF0-A959-4232-AADD-40DB2260F557',
@p14='2018-11-10 14:29:25.5363034',
@p15='8288D9AC-FBCE-417E-89EF-82266B284B78',
@p16=NULL,
@p17='2018-11-10 14:29:25.5363039',
@p18='35C38DF0-A959-4232-AADD-40DB2260F557',
@p19='2018-11-10 14:29:25.5363042',
@p20='4BCFE9F8-E4A5-49F0-B6EE-44871632A903',
@p21='35C38DF0-A959-4232-AADD-40DB2260F557',
@p22='2018-11-10 14:29:25.5363047',
@p23='35C38DF0-A959-4232-AADD-40DB2260F557',
@p24='2018-11-10 14:29:25.5363047'

Я подозреваю, что эта проблема имеет корни в строке кода "IsUnique(false"), которую я выделил во втором фрагменте, поскольку это происходит только для свойства / столбца AddedByAccountId. Однако, если я не включу эту строку, то созданный скрипт миграции добавит уникальное ограничение (которое мне не нужно).

Кто-нибудь сталкивался с проблемами при создании ограничений, когда они не должны или (что более важно) значения NULL, заменяющие фактические данные при вставке?

Приветствие.

1 Ответ

0 голосов
/ 10 ноября 2018

// По какой-то причине этот ключ с собственной ссылкой генерирует уникальный
// ограничение в сценарии миграции, если мы не установим это здесь.

// «ModifiedByAccountId» не имеет этой проблемы! Я подозреваю, что это может
// основная причина проблемы с данными NULL.

Вы подозреваете, что это правильно. EF Core сбит с толку из-за 2-х самоссылающихся навигационных свойств и ошибочно решает (может быть ошибкой), что они представляют собой одно отношение один к одному :

Отношения «один к одному» имеют свойство ссылочной навигации с обеих сторон. Они следуют тем же соглашениям, что и отношения «один ко многим», но для свойства внешнего ключа введен уникальный индекс, чтобы гарантировать, что с каждым принципалом связан только один зависимый элемент.

Конечно, вам нужны два отношения «один ко многим», поэтому вместо того, чтобы фиксировать индекс (который не поможет, как вы уже видели), просто сопоставьте их явно:

modelBuilder.Entity<Account>().HasOne(e => e.AddedByAccount).WithMany();
modelBuilder.Entity<Account>().HasOne(e => e.ModifiedByAccount).WithMany();
...