I am trying to write a SELECT... FOR UPDATE query using Entity Framework (for a Postgresql db). The problem is that using any IsolationLevel, the second request will only block after updating the data (step 2 in the picture) and not from the first (getting the response step in the picture). Chaos level is not supported (not sure why).
I need the resources to be locked even from the selection, otherwise both requests will take the same resources!
I can't find a solution using EF, but this will work using a SQL function (+migration) which is not the preferred choice!
thanks
// Serializable Summary
// A range lock is placed on the System.Data.DataSet, preventing other users from
// updating or inserting rows into the dataset until the transaction is complete.
await using var transaction = await _dbContext.Database.BeginTransactionAsync(
System.Data.IsolationLevel.Serializable,
cancellationToken);
try
{
var query = _dbContext.ValidationRequests
.Where(vr => vr.State == Statuses.New)
.OrderBy(vr => vr.Id);
var response = await PagedList<Model>.GetPagedListAsync(
query,
page,
pageSize,
cancellationToken
);
await UpdateRangeAsync(response.Items, cancellationToken);
await transaction.CommitAsync(cancellationToken);
return response;
}
catch (Exception)
{
await transaction.RollbackAsync(cancellationToken);
throw;
}
GetPagedListAsync method:
public static async Task<PagedList<T>> GetPagedListAsync(
IQueryable<T> query,
int page,
int pageSize,
CancellationToken cancellationToken = default)
{
int totalCount = await query.CountAsync(cancellationToken);
var items = await query
.Skip((page - 1) * pageSize)
.Take(pageSize)
.ToListAsync(cancellationToken);
return new(page, pageSize, items, totalCount);
}
and the update method:
private async Task UpdateRangeAsync(List<Model> requests, CancellationToken cancellationToken = default)
{
if (!requests.Any())
{
return;
}
requests.ForEach(item => item.State = Statuses.Pending);
_dbContext.UpdateRange(requests);
await _dbContext.SaveChangesAsync(cancellationToken);
}
You can use the new
ExecuteUpdateAsyncfeature to do a joinedUPDATEstatement. This means you don't need a transaction as the whole statement is atomic anyway.Something like this would work, although I'm not entirely clear on your object model.
Note that there is no
ToListanywhere, because that would cause client-side evaluation and doesn't work withExecuteUpdate.Do note that
SkipTakecan be highly inefficient even with a supporting index, and you would be better off using Keyset Pagination.If you need the actual rows then you probably want a
RETURNINGorOUTPUTclause (depending on your DBMS), in which case a raw SQL query is probably best.Unfortunately, Postgres does not allow
LIMITdirectly on the target table so you need a self join.In SQL Server you could update the subquery directly