Visitar URL original
Locking rows for update | SQLProvider

SQLProvider

Locking rows for update

Most code needs no lock

Seq.lockForUpdate is for a few named places in a system. It is not a default, and not something to add to be on the safe side. Without it the database already does this:

What is left is the lost update described below, and only where two requests change the same row at the same moment. Rows that belong to one user seldom meet that.

What a lock costs

Where a lock is right: the lost update

A request reads a value, decides on it, and writes a value computed from it. When two requests do that to the same row at the same moment, both read the old value, and the second write undoes or doubles the first:

  1. Request A reads an account's balance: 50.
  2. Request B reads the same balance: 50.
  3. A pays out 40 and writes 10.
  4. B pays out 40 and writes 10. 80 went out of 50, and the balance says 10.

At read committed both transactions see the committed 50, and each one's write is valid on its own. Serializable fails one of the two. A lock makes B wait instead, so both requests get an answer.

Lock when all of these hold:

Otherwise use a transaction, a unique constraint, or the check at save time at the end of this page.

Locking the rows you read

Seq.lockForUpdate asks the database to lock each row the query returns until your transaction ends. Another transaction that wants to lock, change or delete one of those rows waits until yours commits or rolls back, and then reads the new value. In the example, B waits at step 2 until A has written 10, reads 10, and refuses the payment. Plain reads that don't lock aren't blocked.

In SQL this is SELECT ... FOR UPDATE, a "pessimistic lock": the database holds others back, instead of your code noticing a clash afterwards.

Read, decide and write in one transaction. With async code, the TransactionScope needs TransactionScopeAsyncFlowOption.Enabled, or the transaction doesn't follow the awaits:

open System.Transactions

let addToFreight (orderId: int64) (amount: decimal) =
    task {
        let ctx = sql.GetDataContext()
        use scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled)

        // Locked until the scope completes or is disposed
        let! order =
            query {
                for order in ctx.Main.Orders do
                where (order.OrderId = orderId)
                select order
            }
            |> Seq.lockForUpdateHeadAsync

        order.Freight <- order.Freight + amount
        do! ctx.SubmitUpdatesAsync()
        scope.Complete()
    }

The functions

Function

Does

Seq.lockForUpdate

Marks a query to lock its rows; run it with any executor, Seq.toList included.

Seq.lockForUpdateAsync, Array.lockForUpdateAsync, List.lockForUpdateAsync

Run the query with its rows locked.

Seq.lockForUpdateHeadAsync, Seq.lockForUpdateTryHeadAsync

Lock and read one row; tryHead... gives None when there's none. On Oracle not after a sort.

What each database does

Database

SQL

Notes

PostgreSQL

FOR UPDATE

With a left outer join, FOR UPDATE OF the other tables: the rows of the outer-joined table aren't locked.

MySQL (InnoDB)

FOR UPDATE

Oracle

FOR UPDATE

Not with skip, nor with take or head after a sort: refused.

Firebird

FOR UPDATE WITH LOCK

One table only: a query with joins is refused. FOR UPDATE alone doesn't lock in Firebird.

SQL Server

WITH (UPDLOCK, ROWLOCK) on every table

Held until the transaction ends.

SQLite, MS Access, DuckDB, ODBC

none

No row locks: refused with an error. Use a transaction that writes first, or a unique constraint.

A query that isn't one table's rows can't be locked, so a distinct, grouped, counted or aggregated query, or a union, is refused. The lock belongs to a whole statement, so a locked query is also refused as a part of another: as a sub-query (IN, EXISTS), as one side of a union, as the table joined into a query, and in a delete. Lock the outer query instead. In unit tests the mock context's query comes back as it is.

Rules of thumb

Check at save time (optimistic concurrency)

An update writes the columns you changed to the row with that primary key. It doesn't check whether another request changed the row after you read it: the last write wins. When a row is read, shown to a user and saved later, no lock can be held in between. Check at save time instead: read the row again, compare it with what the user saw, and write only when it is the same.

The second read and the write are one unit, or a change could come between them. The sample locks the one row for the length of the save. At Serializable the lock can be left out: a clash then fails one of the transactions.

/// Saves the new freight unless the order changed after the caller read it.
let saveFreight (orderId: int64) (freightSeen: decimal) (newFreight: decimal) =
    task {
        use scope =
            new TransactionScope(
                TransactionScopeOption.Required,
                TransactionOptions(IsolationLevel = System.Transactions.IsolationLevel.ReadCommitted),
                TransactionScopeAsyncFlowOption.Enabled)

        let ctx = sql.GetDataContext()

        let! order =
            query {
                for order in ctx.Main.Orders do
                where (order.OrderId = orderId)
                select order
            }
            |> Seq.lockForUpdateHeadAsync

        if order.Freight <> freightSeen then
            // Someone else saved first: show the user the new values
            return false
        else
            order.Freight <- newFreight
            do! ctx.SubmitUpdatesAsync()
            scope.Complete()
            return true
    }

A column that changes with every write makes the comparison one value: SQL Server's rowversion (timestamp), which the database sets itself and SQLProvider reads as a byte array, or a counter the code raises with each save.

When the rule is about rows that don't exist yet ("one open order per customer"), a unique constraint states it best: the database refuses the second insert, and OnConflict on the insert decides what happens instead (see CRUD).

Multiple items
type LiteralAttribute = inherit Attribute new: unit -> LiteralAttribute

--------------------
new: unit -> LiteralAttribute
val resolutionPath: string = "C:\git\SQLProvider\docs\content\core/../../files/sqlite"
val connectionString: string = "Data Source=C:\git\SQLProvider\docs\content\core\..\northwindEF.db;Version=3;Read Only=false;FailIfMissing=True;"
Multiple items
namespace FSharp

--------------------
namespace Microsoft.FSharp
Multiple items
namespace FSharp.Data

--------------------
namespace Microsoft.FSharp.Data
namespace FSharp.Data.Sql
type sql = SqlDataProvider<...>
type SqlDataProvider
<summary>Typed representation of a database</summary> <param name='ConnectionString'>The connection string for the SQL database</param> <param name='ConnectionStringName'>The connection string name to select from a configuration file</param> <param name='DatabaseVendor'> The target database vendor</param> <param name='IndividualsAmount'>The amount of sample entities to project into the type system for each SQL entity type. Default 50. Note GDPR/PII regulations if using individuals with ContextSchemaPath.</param> <param name='UseOptionTypes'>If set, F# option types will be used in place of nullable database columns. If not, you will always receive the default value of the column's type even if it is null in the database.</param> <param name='ResolutionPath'>The location to look for dynamically loaded assemblies containing database vendor specific connections and custom types. Types used in design-time: If no better clue, prefer .NET Standard 2.0 versions. Semicolon to separate multiple.</param> <param name='Owner'>Oracle: The owner of the schema for this provider to resolve. PostgreSQL: A list of schemas to resolve, separated by spaces, newlines, commas, or semicolons.</param> <param name='CaseSensitivityChange'>Should we do ToUpper or ToLower when generating table names?</param> <param name='TableNames'>Comma separated table names list to limit a number of tables in big instances. The names can have '%' sign to handle it as in the 'LIKE' query (Oracle and MSSQL Only)</param> <param name='ContextSchemaPath'>The location of the context schema previously saved with SaveContextSchema. When not empty, will be used to populate the database schema instead of retrieving it from then database.</param> <param name='OdbcQuote'>Odbc quote characters: Quote characters for the table and column names: `alias`, [alias]</param> <param name='SQLiteLibrary'>Use System.Data.SQLite or Mono.Data.SQLite or select automatically (SQLite only)</param> <param name='SsdtPath'>A path to an SSDT .dacpac file.'</param>
namespace FSharp.Data.Sql.Common
type DatabaseProviderTypes = | MSSQLSERVER = 0 | SQLITE = 1 | POSTGRESQL = 2 | MYSQL = 3 | ORACLE = 4 | MSACCESS = 5 | ODBC = 6 | FIREBIRD = 7 | MSSQLSERVER_DYNAMIC = 8 | MSSQLSERVER_SSDT = 9 | DUCKDB = 10 | EXTERNAL = 11
<summary> Specifies the database provider type for the SQL type provider. Each provider has its own specific implementation for SQL generation and data type mapping. </summary>
Common.DatabaseProviderTypes.SQLITE: Common.DatabaseProviderTypes = 1
<summary> SQLite database using System.Data.SQLite or Microsoft.Data.Sqlite </summary>
type CaseSensitivityChange = | ORIGINAL = 0 | TOUPPER = 1 | TOLOWER = 2
<summary> Specifies how to handle case sensitivity when generating table and column names. </summary>
Common.CaseSensitivityChange.ORIGINAL: Common.CaseSensitivityChange = 0
<summary> Keep original casing from the database </summary>
type SQLiteLibrary = | SystemDataSQLite = 0 | MonoDataSQLite = 1 | AutoSelect = 2 | MicrosoftDataSqlite = 3
<summary> Specifies which SQLite library to use for connections. Different libraries may have different capabilities and platform support. </summary>
Common.SQLiteLibrary.SystemDataSQLite: Common.SQLiteLibrary = 0
<summary> .NET Framework default </summary>
namespace System
namespace System.Transactions
val addToFreight: orderId: int64 -> amount: decimal -> System.Threading.Tasks.Task<unit>
val orderId: int64
Multiple items
val int64: value: 'T -> int64 (requires member op_Explicit)

--------------------
type int64 = System.Int64

--------------------
type int64<'Measure> = int64
val amount: decimal
Multiple items
val decimal: value: 'T -> decimal (requires member op_Explicit)

--------------------
type decimal = System.Decimal

--------------------
type decimal<'Measure> = decimal
val task: TaskBuilder
val ctx: SqlDataProvider<...>.dataContext
SqlDataProvider<...>.GetDataContext() : SqlDataProvider<...>.dataContext
<summary>Returns an instance of the SQL Provider using the static parameters</summary>
   (+0 other overloads)
SqlDataProvider<...>.GetDataContext(selectOperations: SelectOperations) : SqlDataProvider<...>.dataContext
<summary>Returns an instance of the SQL Provider using the static parameters</summary><param name='selectOperations'>Execute select-clause operations in SQL database rather than .NET-side.</param>
   (+0 other overloads)
SqlDataProvider<...>.GetDataContext(commandTimeout: int) : SqlDataProvider<...>.dataContext
<summary>Returns an instance of the SQL Provider using the static parameters</summary><param name='commandTimeout'>SQL command timeout. Maximum time for single SQL-command in seconds.</param>
   (+0 other overloads)
SqlDataProvider<...>.GetDataContext(transactionOptions: Transactions.TransactionOptions) : SqlDataProvider<...>.dataContext
<summary>Returns an instance of the SQL Provider using the static parameters</summary><param name='transactionOptions'>TransactionOptions for the transaction created on SubmitChanges.</param>
   (+0 other overloads)
SqlDataProvider<...>.GetDataContext(connectionString: string) : SqlDataProvider<...>.dataContext
<summary>Returns an instance of the SQL Provider using the static parameters</summary><param name='connectionString'>The database runtime connection string</param>
   (+0 other overloads)
SqlDataProvider<...>.GetDataContext(connectionString: string, selectOperations: SelectOperations) : SqlDataProvider<...>.dataContext
<summary>Returns an instance of the SQL Provider using the static parameters</summary><param name='connectionString'>The database runtime connection string</param><param name='selectOperations'>Execute select-clause operations in SQL database rather than .NET-side.</param>
   (+0 other overloads)
SqlDataProvider<...>.GetDataContext(transactionOptions: Transactions.TransactionOptions, commandTimeout: int) : SqlDataProvider<...>.dataContext
<summary>Returns an instance of the SQL Provider using the static parameters</summary><param name='transactionOptions'>TransactionOptions for the transaction created on SubmitChanges.</param><param name='commandTimeout'>SQL command timeout. Maximum time for single SQL-command in seconds.</param>
   (+0 other overloads)
SqlDataProvider<...>.GetDataContext(connectionString: string, commandTimeout: int) : SqlDataProvider<...>.dataContext
<summary>Returns an instance of the SQL Provider using the static parameters</summary><param name='connectionString'>The database runtime connection string</param><param name='commandTimeout'>SQL command timeout. Maximum time for single SQL-command in seconds.</param>
   (+0 other overloads)
SqlDataProvider<...>.GetDataContext(connectionString: string, transactionOptions: Transactions.TransactionOptions) : SqlDataProvider<...>.dataContext
<summary>Returns an instance of the SQL Provider using the static parameters</summary><param name='connectionString'>The database runtime connection string</param><param name='transactionOptions'>TransactionOptions for the transaction created on SubmitChanges.</param>
   (+0 other overloads)
SqlDataProvider<...>.GetDataContext(connectionString: string, resolutionPath: string) : SqlDataProvider<...>.dataContext
<summary>Returns an instance of the SQL Provider using the static parameters</summary><param name='connectionString'>The database runtime connection string</param><param name='resolutionPath'>The location to look for dynamically loaded assemblies containing database vendor specific connections and custom types. Types used in design-time: If no better clue, prefer .NET Standard 2.0 versions. Semicolon to separate multiple.</param>
   (+0 other overloads)
val scope: TransactionScope
Multiple items
type TransactionScope = interface IDisposable new: unit -> unit + 13 overloads member Complete: unit -> unit member Dispose: unit -> unit
<summary>Makes a code block transactional. This class cannot be inherited.</summary>

--------------------
TransactionScope() : TransactionScope
   (+0 other overloads)
TransactionScope(transactionToUse: Transaction) : TransactionScope
   (+0 other overloads)
TransactionScope(asyncFlowOption: TransactionScopeAsyncFlowOption) : TransactionScope
   (+0 other overloads)
TransactionScope(scopeOption: TransactionScopeOption) : TransactionScope
   (+0 other overloads)
TransactionScope(transactionToUse: Transaction, scopeTimeout: System.TimeSpan) : TransactionScope
   (+0 other overloads)
TransactionScope(transactionToUse: Transaction, asyncFlowOption: TransactionScopeAsyncFlowOption) : TransactionScope
   (+0 other overloads)
TransactionScope(scopeOption: TransactionScopeOption, scopeTimeout: System.TimeSpan) : TransactionScope
   (+0 other overloads)
TransactionScope(scopeOption: TransactionScopeOption, transactionOptions: TransactionOptions) : TransactionScope
   (+0 other overloads)
TransactionScope(scopeOption: TransactionScopeOption, asyncFlowOption: TransactionScopeAsyncFlowOption) : TransactionScope
   (+0 other overloads)
TransactionScope(transactionToUse: Transaction, scopeTimeout: System.TimeSpan, interopOption: EnterpriseServicesInteropOption) : TransactionScope
   (+0 other overloads)
type TransactionScopeAsyncFlowOption = | Suppress = 0 | Enabled = 1
<summary>Specifies whether transaction flow across thread continuations is enabled for <see cref="T:System.Transactions.TransactionScope" />.</summary>
field TransactionScopeAsyncFlowOption.Enabled: TransactionScopeAsyncFlowOption = 1
val order: SqlDataProvider<...>.dataContext.main.OrdersEntity
val query: Linq.QueryBuilder
property SqlDataProvider<...>.dataContext.Main: SqlDataProvider<...>.dataContext.mainSchema with get
property SqlDataProvider<...>.dataContext.mainSchema.Orders: SqlDataProvider<...>.dataContext.mainSchema.main.Orders with get
<summary> The table Orders belonging to schema main</summary>
custom operation: where (bool) Calls Linq.QueryBuilder.Where
property SqlDataProvider<...>.dataContext.main.OrdersEntity.OrderId: int64 with get, set
<summary>OrderID: integer</summary>
custom operation: select ('Result) Calls Linq.QueryBuilder.Select
Multiple items
module Seq from FSharp.Data.Sql

--------------------
module Seq from Microsoft.FSharp.Collections
val lockForUpdateHeadAsync: query: System.Linq.IQueryable<'T> -> System.Threading.Tasks.Task<'T>
<summary>Reads the query's first row, locked until the transaction ends (see <c>lockForUpdate</c>), releasing the OS thread while the database works.</summary>
<param name="query">The query, as <c>lockForUpdate</c> takes it.</param>
<returns>The row, locked. Fails when the query reads no rows; see <c>lockForUpdateTryHeadAsync</c>.</returns>
<remarks>Oracle refuses it after a sort.</remarks>
SqlDataProvider<...>.dataContext.SubmitUpdatesAsync() : System.Threading.Tasks.Task
<summary>Save changes to data-source. May throws errors: Use Async.Catch and to deal with non-saved items use GetUpdates() and ClearUpdates().</summary>
TransactionScope.Complete() : unit
val saveFreight: orderId: int64 -> freightSeen: decimal -> newFreight: decimal -> System.Threading.Tasks.Task<bool>
 Saves the new freight unless the order changed after the caller read it.
val freightSeen: decimal
val newFreight: decimal
type TransactionScopeOption = | Required = 0 | RequiresNew = 1 | Suppress = 2
<summary>Provides additional options for creating a transaction scope.</summary>
field TransactionScopeOption.Required: TransactionScopeOption = 0
type TransactionOptions = member Equals: obj: obj -> bool + 1 overload member GetHashCode: unit -> int static member (<>) : x: TransactionOptions * y: TransactionOptions -> bool static member (=) : x: TransactionOptions * y: TransactionOptions -> bool member IsolationLevel: IsolationLevel member Timeout: TimeSpan
<summary>Contains additional information that specifies transaction behaviors.</summary>
type IsolationLevel = | Serializable = 0 | RepeatableRead = 1 | ReadCommitted = 2 | ReadUncommitted = 3 | Snapshot = 4 | Chaos = 5 | Unspecified = 6
<summary>Specifies the isolation level of a transaction.</summary>
field IsolationLevel.ReadCommitted: IsolationLevel = 2

Type something to start searching.