Как удалить строку с отношением 1:1 к той же таблице?

Я использую Entity Framework Core, и у меня есть таблица:

public class BlogComment
{
    public int Id { get; set; }
    public BlogPost Post { get; set; }
    [StringLength(100)]
    public string AuthorName { get; set; }
    [StringLength(254)]
    public string AuthorEmail { get; set; }
    public bool SendMailOnReply { get; set; }
    [StringLength(2000)]
    public string Content { get; set; }
    public DateTime CreatedTime { get; set; }
    public int? ReplyToId { get; set; }
    public BlogComment ReplyTo { get; set; }
}

Из этого EFC генерирует следующую таблицу:

CREATE TABLE [dbo].[BlogComment] (
    [Id]              INT             IDENTITY (1, 1) NOT NULL,
    [AuthorEmail]     NVARCHAR (254)  NULL,
    [AuthorName]      NVARCHAR (100)  NULL,
    [Content]         NVARCHAR (2000) NULL,
    [CreatedTime]     DATETIME2 (7)   NOT NULL,
    [PostId]          INT             NULL,
    [ReplyToId]       INT             NULL,
    [SendMailOnReply] BIT             NOT NULL,
    CONSTRAINT [PK_BlogComment] PRIMARY KEY CLUSTERED ([Id] ASC),
    CONSTRAINT [FK_BlogComment_BlogPost_PostId] FOREIGN KEY ([PostId]) REFERENCES [dbo].[BlogPost] ([Id]),
    CONSTRAINT [FK_BlogComment_BlogComment_ReplyToId] FOREIGN KEY ([ReplyToId]) REFERENCES [dbo].[BlogComment] ([Id])
);
GO
CREATE NONCLUSTERED INDEX [IX_BlogComment_PostId]
    ON [dbo].[BlogComment]([PostId] ASC);
GO
CREATE UNIQUE NONCLUSTERED INDEX [IX_BlogComment_ReplyToId]
    ON [dbo].[BlogComment]([ReplyToId] ASC) WHERE ([ReplyToId] IS NOT NULL);

Некоторые комментарии отправляются в качестве ответа на другие, но не все. Когда исходный комментарий удаляется, ответ становится обычным комментарием. Итак, следуя этому руководству, конфигурация выглядит так: это:

modelBuilder.Entity<BlogComment>()
      .HasOne(p => p.ReplyTo)
      .WithOne()
      .HasForeignKey<BlogComment>(c => c.ReplyToId)
      .IsRequired(false)
      .OnDelete(DeleteBehavior.SetNull);

Метод удаления довольно прост:

var comment = await context.BlogComment.Include(c => c.ReplyTo).SingleAsync(m => m.Id == id);
context.BlogComment.Remove(comment);
await context.SaveChangesAsync();

Но запустить не могу, выдает ошибку:

System.Data.SqlClient.SqlException: инструкция DELETE конфликтует с ограничением SAME TABLE REFERENCE «FK_BlogComment_BlogComment_ReplyToId».

Как я могу это исправить?


person klenium    schedule 12.05.2018    source источник
comment
Удалите .Include(c => c.ReplyTo) из своего кода   -  person vivek nuna    schedule 13.05.2018
comment
@viveknuna Сначала я его не включал, а теперь удалил, но получаю ту же ошибку.   -  person klenium    schedule 13.05.2018
comment
можете ли вы проверить, является ли ReplyToId нулевым в таблице базы данных?   -  person vivek nuna    schedule 13.05.2018
comment
@viveknuna Да, это так. Я добавил код таблицы к вопросу. В таблице прямо сейчас есть несколько комментариев, которые не являются ответом на другой, т.е. ReplyToId имеет значение null.   -  person klenium    schedule 13.05.2018
comment
Извините, тогда я понятия не имею. Может быть, вы можете попробовать comment.ReplyTo = null; перед сохранением изменений.   -  person vivek nuna    schedule 13.05.2018
comment
Когда я пытаюсь это сделать (используя текущую стабильную версию ядра EF 2.0.3), я получаю печально известное исключение, заключающееся в том, что FK вызывает циклы или несколько каскадных путей. Это ограничение SQL Server, от которого невозможно обойти. Какая у вас версия EF? Кажется, что реализация изменилась в то же время.   -  person Gert Arnold    schedule 13.05.2018
comment
@GertArnold тоже 2.0.3, но у меня options.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore   -  person klenium    schedule 13.05.2018
comment
Это не имеет значения. Я даже не могу создать таблицу.   -  person Gert Arnold    schedule 13.05.2018
comment
Кстати, соотношение должно быть 1:n: WithMany вместо WithOne. Тем не менее, все еще не может создать таблицу. Я удивлен, что у тебя нет этой проблемы.   -  person Gert Arnold    schedule 13.05.2018
comment
Оооо. Спасибо, вы правы, на комментарий может быть больше ответов... Этот вопрос стоило опубликовать. :D У меня тоже была эта циклическая ошибка, но я мог как-то решить это в начале.   -  person klenium    schedule 13.05.2018


Ответы (3)


Чтобы завершить разговор в комментариях:

Во-первых, ссылка на себя представляет собой ассоциацию 1:n:

modelBuilder.Entity<BlogComment>()
      .HasOne(p => p.ReplyTo)
      .WithMany(c => c.Replies)
      .HasForeignKey(c => c.ReplyToId)
      .IsRequired(false)
      .OnDelete(<we'll get to that>);

Итак, просто для удобства BlogComment теперь также имеет свойство

public ICollection<BlogComment> Replies { get; set; }

Однако я не могу создать таблицу, используя

.OnDelete(DeleteBehavior.SetNull);

Это дает мне

Введение ограничения FOREIGN KEY «FK_BlogComments_BlogComments_ReplyToId» в таблице «BlogComments» может вызвать циклы или множественные каскадные пути.

Это ограничение Sql Server, которое мы просто должны принять, обойти его невозможно. Единственный способ получить желаемое поведение каскада - это

.OnDelete(DeleteBehavior.ClientSetNull);

Что:

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

То есть: клиент выполняет SQL, чтобы обнулить значения внешнего ключа. Однако дочерние записи должны отслеживаться. Чтобы удалить родителя BlogComment, действие удаления должно выглядеть так:

using (var db = new MyContext(connectionString))
{
    var c1 = db.BlogComments
        .Include(c => c.Replies) // Children should be included
        .SingleOrDefault(c => c.Id == 1);
    db.BlogComments.Remove(c1);
    db.SaveChanges();
}

Как видите, вам не нужно устанавливать ReplyToId = null, об этом позаботится EF.

person Gert Arnold    schedule 13.05.2018
comment
Это, однако, не 1: 1, вы можете описать и этот случай в отношении исходного вопроса. Хотя мне будет полезно, спасибо. - person klenium; 13.05.2018
comment
Случай 1:0..1 в EF-core не сильно отличается. Структура таблицы может быть идентичной (то есть отдельный внешний ключ, а не отношение общего первичного ключа, как предусмотрено EF6). Применяются те же каскадные ограничения, и для обеспечения работы DeleteBehavior.ClientSetNull необходимо отслеживать как родительский, так и дочерний элементы. - person Gert Arnold; 13.05.2018
comment
Это действительно правильный ответ. Единственная разница с один к одному — уникальное ограничение в последнем случае. Но существенной частью является ClientSetNull и свойство обратной навигации. В случае один к одному замените Replies на public BlogComment Reply { get; set; }, WithOne(c => c.Reply) и Include(c -> c.Reply) - person Ivan Stoev; 13.05.2018

Что касается меня, мне нужно было Include() сущностей, с которыми мне нужно было «разобраться», когда я удалял сущность. EF не может управлять тем, что в данный момент не отслеживается.

var breedToDelete = context.Breed
    .Include(x => x.Cats)
    .Single(x => x.Id == testBreedId);

context.Breed.Remove(breedToDelete);
context.SaveChanges();
person Victorio Berra    schedule 02.08.2018

Я мог бы заставить его работать, вручную установив ReplyTo на ноль. Я все еще ищу лучшее решение или объяснение, почему это необходимо. Разве не это должен делать OnDelete(DeleteBehavior.SetNull)?

var comment = await context.BlogComment.Include(c => c.ReplyTo).SingleAsync(m => m.Id == id);
var reply = await context.BlogComment.SingleOrDefaultAsync(m => m.ReplyToId == id);
if (reply != null)
{
    reply.ReplyTo = null;
    reply.ReplyToId = null;
    context.Entry(reply).State = EntityState.Modified;
}
context.BlogComment.Remove(comment);
person klenium    schedule 13.05.2018