Концепция
После долгих исследований я нашел решение проблемы:
Я добавил строку 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 будут пропущены при сбое приложения при вставке сообщения.