Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 39 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ Install-Package PhenX.EntityFrameworkCore.BulkInsert.MySql

## Usage

1. Register the bulk insert provider in your `DbContextOptions`:
Register the bulk insert provider in your `DbContextOptions`:

```csharp
services.AddDbContext<MyDbContext>(options =>
Expand All @@ -62,7 +62,7 @@ services.AddDbContext<MyDbContext>(options =>
});
```

2. Use the bulk insert extension method:
### Very basic usage

```csharp
// Asynchronously
Expand All @@ -72,7 +72,7 @@ await dbContext.ExecuteBulkInsertAsync(entities);
dbContext.ExecuteBulkInsert(entities);
```

3. You can also configure the bulk insert options:
### Bulk insert with options

```csharp
// Common options
Expand Down Expand Up @@ -103,12 +103,47 @@ await dbContext.ExecuteBulkInsertAsync(entities, o =>
});
```

4. You can also return the inserted entities (slower):
### Returning inserted entities

```csharp
await dbContext.ExecuteBulkInsertReturnEntitiesAsync(entities);
```

### Conflict resolution / merge / upsert

Conflict resolution works by specifying columns that should be used to detect conflicts and the action to take when
a conflict is detected (e.g., update existing rows), using the `onConflict` parameter.

* The conflicting columns are specified with the `Match` property and must have a unique constraint in the database.
* The action to take when a conflict is detected is specified with the `Update` property. If not specified, the default action is to do nothing (i.e., skip the conflicting rows).
* You can also specify the condition for the update action using either the `Where` or the `RawWhere` property. If not specified, the update action will be applied to all conflicting rows.

```csharp
await dbContext.ExecuteBulkInsertAsync(entities, onConflict: new OnConflictOptions<TestEntity>
{
Match = e => new
{
e.Name,
// ...other columns to match on
},

// Optional: specify the update action, if not specified, the default action is to do nothing
// Excluded is the row being inserted which is in conflict, and Inserted is the row already in the database.
Update = (inserted, excluded) => new TestEntity
{
Price = inserted.Price // Update the Price column with the new value
},

// Optional: specify the condition for the update action
// Excluded is the row being inserted which is in conflict, and Inserted is the row already in the database.
// Using raw SQL condition
RawWhere = (insertedTable, excludedTable) => $"{excludedTable}.some_price > {insertedTable}.some_price",

// OR using a lambda expression
Where = (inserted, excluded) => excluded.Price > inserted.Price,
});
```

## Roadmap

- [ ] [Add support for navigation properties](https://github.com/PhenX/PhenX.EntityFrameworkCore.BulkInsert/issues/2)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
using System.Text;

using Microsoft.EntityFrameworkCore;

using PhenX.EntityFrameworkCore.BulkInsert.Dialect;
using PhenX.EntityFrameworkCore.BulkInsert.Metadata;
using PhenX.EntityFrameworkCore.BulkInsert.Options;
Expand All @@ -12,14 +14,22 @@ internal class MySqlServerDialectBuilder : SqlDialectBuilder

protected override string CloseDelimiter => "`";

/// <inheritdoc />
protected override bool SupportsMoveRows => false;

/// <inheritdoc />
protected override bool SupportsInsertIntoAlias => false;

public override string CreateTableCopySql(string tempNameName, TableMetadata tableInfo, IReadOnlyList<ColumnMetadata> columns)
{
return $"CREATE TEMPORARY TABLE {tempNameName} SELECT * FROM {tableInfo.QuotedTableName} WHERE 1 = 0;";
}

protected override void AppendConflictCondition<T>(StringBuilder sql, OnConflictOptions<T> onConflictTyped)
protected override void AppendConflictCondition<T>(
StringBuilder sql,
TableMetadata target,
DbContext context,
OnConflictOptions<T> onConflictTyped)
{
throw new NotSupportedException("Conflict conditions are not supported in MYSQL");
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
using System.Text;

using Microsoft.EntityFrameworkCore;

using PhenX.EntityFrameworkCore.BulkInsert.Dialect;
using PhenX.EntityFrameworkCore.BulkInsert.Metadata;
using PhenX.EntityFrameworkCore.BulkInsert.Options;
Expand Down Expand Up @@ -34,7 +36,10 @@ public override string CreateTableCopySql(string tempTableName, TableMetadata ta
return q.ToString();
}

protected override string Trim(string lhs) => $"TRIM({lhs})";

public override string BuildMoveDataSql<T>(
DbContext context,
TableMetadata target,
string source,
IReadOnlyList<ColumnMetadata> insertedColumns,
Expand Down Expand Up @@ -66,24 +71,37 @@ public override string BuildMoveDataSql<T>(
throw new InvalidOperationException("Table has no primary key that can be used for conflict detection.");
}

q.AppendLine($"MERGE INTO {target.QuotedTableName} AS TARGET");
q.AppendLine($"MERGE INTO {target.QuotedTableName} AS {PseudoTableInserted}");

q.Append("USING (SELECT ");
q.AppendColumns(insertedColumns);
q.Append($" FROM {source}) AS SOURCE (");
q.Append($" FROM {source}) AS {PseudoTableExcluded} (");
q.AppendColumns(insertedColumns);
q.AppendLine(")");

q.Append("ON ");
q.AppendJoin(" AND ", matchColumns, (b, col) => b.Append($"TARGET.{col} = SOURCE.{col}"));
q.AppendJoin(" AND ", matchColumns, (b, col) => b.Append($"{PseudoTableInserted}.{col} = {PseudoTableExcluded}.{col}"));
q.AppendLine();

if (onConflictTyped.Update != null)
{
var columns = target.GetColumns(false);

q.AppendLine("WHEN MATCHED THEN UPDATE SET ");
q.AppendJoin(", ", GetUpdates(target, columns, onConflictTyped.Update));
q.AppendLine("WHEN MATCHED ");

if (onConflictTyped.RawWhere != null || onConflictTyped.Where != null)
{
if (onConflictTyped is { RawWhere: not null, Where: not null })
{
throw new ArgumentException("Cannot specify both RawWhere and Where in OnConflictOptions.");
}

q.Append("AND ");
AppendConflictCondition(q, target, context, onConflictTyped);
}

q.AppendLine("THEN UPDATE SET ");
q.AppendJoin(", ", GetUpdates(context, target, columns, onConflictTyped.Update));
q.AppendLine();
}

Expand All @@ -92,13 +110,13 @@ public override string BuildMoveDataSql<T>(
q.AppendLine(")");

q.Append("VALUES (");
q.AppendJoin(", ", insertedColumns, (b, col) => b.Append($"SOURCE.{col.QuotedColumName}"));
q.AppendJoin(", ", insertedColumns, (b, col) => b.Append($"{PseudoTableExcluded}.{col.QuotedColumName}"));
q.AppendLine(")");

if (returnedColumns.Count != 0)
{
q.Append("OUTPUT ");
q.AppendJoin(", ", returnedColumns, (b, col) => b.Append($"INSERTED.{col.QuotedColumName} AS {col.QuotedColumName}"));
q.AppendJoin(", ", returnedColumns, (b, col) => b.Append($"{PseudoTableInserted}.{col.QuotedColumName} AS {col.QuotedColumName}"));
q.AppendLine();
}
}
Expand All @@ -113,7 +131,7 @@ public override string BuildMoveDataSql<T>(
if (returnedColumns.Count != 0)
{
q.Append("OUTPUT ");
q.AppendJoin(", ", returnedColumns, (b, col) => b.Append($"INSERTED.{col.QuotedColumName} AS {col.QuotedColumName}"));
q.AppendJoin(", ", returnedColumns, (b, col) => b.Append($"{PseudoTableInserted}.{col.QuotedColumName} AS {col.QuotedColumName}"));
q.AppendLine();
}

Expand All @@ -131,12 +149,6 @@ public override string BuildMoveDataSql<T>(
q.AppendLine($"SET IDENTITY_INSERT {target.QuotedTableName} OFF;");
}

var result = q.ToString();
return result;
}

protected override string GetExcludedColumnName(string columnName)
{
return $"SOURCE.{columnName}";
return q.ToString();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,6 @@ public override string CreateTableCopySql(string tempNameName, TableMetadata tab
{
return $"CREATE TEMP TABLE {tempNameName} AS SELECT * FROM {tableInfo.QuotedTableName} WHERE 0;";
}

protected override string Trim(string lhs) => $"TRIM({lhs})";
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;

using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Storage;
Expand Down Expand Up @@ -202,6 +201,7 @@ protected virtual async Task AddBulkInsertIdColumn<T>(
{
var query =
SqlDialect.BuildMoveDataSql<T>(
context,
tableInfo,
tempTableName,
tableInfo.GetColumns(options.CopyGeneratedColumns),
Expand Down
Loading