Skip to content

Support batch SQL updates from IEnumerable parameter sets #62

Open

Description

@twogood

Problem

Dapper.ExecuteAsync(sql, IEnumerable<object>, transaction) accepts a sequence of parameter objects and executes the update once for each object within the supplied transaction. Activout.DatabaseClient currently has no equivalent for an annotated DAO method.

How this was discovered

While migrating fork-order-processing-system to Activout.DatabaseClient 5.0.0-rc4, BookRepository.AddAsync and UpdateAsync needed to insert all book authors after the book row was created or updated.

The previous Dapper implementation used:

var authors = book.Authors.Select(author => new { book.ISBN, author.AuthorName });
await connection.ExecuteAsync(authorSql, authors, transaction);

A DAO method currently builds only one SqlStatement: MethodHandler.AddSqlStatementParameters adds one value for each method argument, and DapperGateway.GetDynamicParameters converts that to one DynamicParameters instance. Passing IEnumerable<Author> as a normal argument therefore does not express Dapper's batch-execution overload.

Current workaround

Keep the operation in the repository transaction and issue one DAO call per author:

using var transaction = database.BeginTransaction();
await database.InsertBookAsync(/* ... */, transaction);

foreach (var author in book.Authors)
    await database.InsertAuthorAsync(book.ISBN, author.AuthorName, transaction);

transaction.Commit();
[SqlUpdate("INSERT INTO BookAuthor (ISBN, AuthorName) VALUES (@isbn, @authorName)")]
Task InsertAuthorAsync(string isbn, string authorName, IDbTransaction transaction);

This preserves correctness and atomicity, and Dapper itself also performs the batch as repeated executions. However, it adds a generated-client dispatch/reflection path for each item and makes the intent less direct.

Suggested API

Add explicit batch binding for SqlUpdate, for example:

[SqlUpdate("INSERT INTO BookAuthor (ISBN, AuthorName) VALUES (@isbn, @authorName)")]
Task<int> InsertAuthorsAsync(
    [BindMany] IEnumerable<BookAuthorInsert> authors,
    IDbTransaction transaction);

Possible implementation:

  1. Add a [BindMany] parameter attribute rather than overloading [BindProperties], whose current semantics are one object with properties.
  2. Have MethodHandler detect it only for SqlUpdate methods and produce multiple parameter sets, preserving the transaction parameter separately.
  3. Extend IDatabaseGateway with a bulk update method, for example Task<int> ExecuteManyAsync(SqlStatement statement, IEnumerable<IReadOnlyList<QueryParameter>> parameterSets).
  4. In DapperGateway, materialize each set as an object/dictionary or DynamicParameters and call Dapper's ExecuteAsync(sql, parameterSets, transaction), returning Dapper's aggregate affected-row count.
  5. Add tests for an empty sequence, multiple rows, [Bind("...")] property aliases, Task and Task<int> returns, rollback behavior, and use with IWithTransactions.

An alternative is an overload accepting IEnumerable<T> plus a mapper, but an explicit attribute preserves the declarative DAO style and avoids ambiguous treatment of enumerable query parameters such as WHERE id = ANY(@ids).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

No labels
No labels

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions