Последовательный идентификатор на внешний ключ в EF Core - PullRequest
0 голосов
/ 09 ноября 2018

Я проектирую базу данных с Entity Framework Core, которая должна содержать два типа сущностей:

  1. Сущность с именем "Канал" с уникальным ChannelId
  2. Сущность с именем «Сообщение» с внешним ключом ChannelId и MessageId

MessageId должен быть уникальным для каждого канала , и его следует считать, начиная с 1.

Моя первая попытка реализовать это состояла в том, чтобы использовать составной ключ для сущности Message с ChannelId и MessageId, но он не должен оставаться таким. Однако я не знаю, как автоматически сгенерировать MessageId с EF Core.

Итак, я попытался получить последние MessageId для текущего канала, увеличил его и попытался вставить:

public class DatabaseContext : DbContext
{
    public void AddMessage(Message message)
    {
        long id = Messages
            .Where(m => m.ChannelId == message.ChannelId)
            .Select(m => m.MessageId)
            .OrderByDescending(i => i)
            .FirstOrDefault()
            + 1;

        while (true)
        {
            try
            {
                message.MessageId = id;
                Messages.Add(insert);
                SaveChanges();
                return;
            }
            catch
            {
                id++;
            }
        }
    }
}

Этот код не работает. После возникновения исключения EF Core не вставляет элемент с увеличенным идентификатором. В дополнение к этому он кажется очень неэффективным в ситуации с одновременными вставками.

Есть ли более элегантное решение для решения этой проблемы, когда я использую дополнительный идентификатор в таблице сообщений в качестве первичного ключа и, возможно, некоторые дополнительные таблицы?

1 Ответ

0 голосов
/ 31 января 2019

Концепция

После долгих исследований я нашел решение проблемы:

Я добавил строку MessageIdCounter в свою таблицу Channels.

В отличие от классического кода, SQL допускает атомарную условную запись. Это может быть использовано для оптимистичной обработки параллелизма. Сначала мы читаем значение счетчика и увеличиваем его. Затем мы пытаемся применить изменения:

UPDATE Channels SET MessageIdCounter = $incrementedValue
WHERE ChannelId = $channelId AND MessageIdCounter = $originalValue;

Сервер базы данных вернет количество изменений. Если не было внесено никаких изменений, MessageIdCounter должно быть изменено за это время. Затем мы должны снова запустить операцию.

Осуществление

Объекты:

public class Channel
{
    public long ChannelId { get; set; }
    public long MessageIdCounter { get; set; }

    public IEnumerable<Message> Messages { get; set; }
}
public class Message
{
    public long MessageId { get; set; }
    public byte[] Content { get; set; }

    public long ChannelId { get; set; }
    public Channel Channel { get; set; }
}

Контекст базы данных:

public class DatabaseContext : DbContext
{
    public DbSet<Channel> Channels { get; set; }
    public DbSet<Message> Messages { get; set; }

    protected override void OnModelCreating(ModelBuilder builder)
    {
        var channel = builder.Entity<Channel>();
        channel.HasKey(c => c.ChannelId);
        channel.Property(c => c.MessageIdCounter).IsConcurrencyToken();

        var message = builder.Entity<Message>();
        message.HasKey(m => new { m.ChannelId, m.MessageId });
        message.HasOne(m => m.Channel).WithMany(c => c.Messages).HasForeignKey(m => m.ChannelId);
    }
}

Полезный метод:

/// <summary>
/// Call this method to retrieve a MessageId for inserting a Message.
/// </summary>
public long GetNextMessageId(long channelId)
{
    using (DatabaseContext ctx = new DatabaseContext())
    {
        bool saved = false;
        Channel channel = ctx.Channels.Single(c => c.ChannelId == channelId);
        long messageId = ++channel.MessageIdCounter;
        do
        {
            try
            {
                ctx.SaveChanges();
                saved = true;
            }
            catch (DbUpdateConcurrencyException ex)
            {
                var entry = ex.Entries.Single();    
                var proposedValues = entry.CurrentValues;
                var databaseValues = entry.GetDatabaseValues();
                const string name = nameof(Channel.MessageIdCounter);
                proposedValues[name] = messageId = (long)databaseValues[name] + 1;
                entry.OriginalValues.SetValues(databaseValues);
            }
        } while (!saved);
        return messageId;
    }
}

Для успешного использования токенов параллелизма EF Core мне пришлось установить изоляцию транзакций MySQL как минимум на READ COMMITTED.

Резюме

Возможно реализовать инкрементный идентификатор для каждого внешнего ключа с помощью EF Core. Это решение не является идеальным, поскольку для одной вставки требуется две транзакции, и поэтому оно медленнее, чем строка с автоинкрементом. Кроме того, возможно, что MessageId s будут пропущены при сбое приложения при вставке сообщения.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...