Skip to content

Commit 960b33c

Browse files
author
fabien.menager
committed
Enhance conflict resolution with support for raw SQL conditions and expressions in OnConflictOptions
1 parent 481d4e8 commit 960b33c

10 files changed

Lines changed: 226 additions & 66 deletions

File tree

README.md

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ Install-Package PhenX.EntityFrameworkCore.BulkInsert.MySql
4343

4444
## Usage
4545

46-
1. Register the bulk insert provider in your `DbContextOptions`:
46+
Register the bulk insert provider in your `DbContextOptions`:
4747

4848
```csharp
4949
services.AddDbContext<MyDbContext>(options =>
@@ -62,7 +62,7 @@ services.AddDbContext<MyDbContext>(options =>
6262
});
6363
```
6464

65-
2. Use the bulk insert extension method:
65+
### Very basic usage
6666

6767
```csharp
6868
// Asynchronously
@@ -72,7 +72,7 @@ await dbContext.ExecuteBulkInsertAsync(entities);
7272
dbContext.ExecuteBulkInsert(entities);
7373
```
7474

75-
3. You can also configure the bulk insert options:
75+
### Bulk insert with options
7676

7777
```csharp
7878
// Common options
@@ -103,12 +103,46 @@ await dbContext.ExecuteBulkInsertAsync(entities, o =>
103103
});
104104
```
105105

106-
4. You can also return the inserted entities (slower):
106+
### Returning inserted entities
107107

108108
```csharp
109109
await dbContext.ExecuteBulkInsertReturnEntitiesAsync(entities);
110110
```
111111

112+
### Conflict resolution / merge / upsert
113+
114+
Conflict resolution works by specifying columns that should be used to detect conflicts and the action to take when
115+
a conflict is detected (e.g., update existing rows), using the `onConflict` parameter.
116+
117+
* The conflicting columns are specified with the `Match` property and must have a unique constraint in the database.
118+
* 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).
119+
* 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.
120+
121+
```csharp
122+
await dbContext.ExecuteBulkInsertAsync(entities, onConflict: new OnConflictOptions<TestEntity>
123+
{
124+
Match = e => new
125+
{
126+
e.Name,
127+
// ...other columns to match on
128+
},
129+
130+
// Optional: specify the update action, if not specified, the default action is to do nothing
131+
Update = e => new TestEntity
132+
{
133+
Price = e.Price // Update the Price column with the new value
134+
},
135+
136+
// Optional: specify the condition for the update action
137+
// Excluded is the row being inserted which is in conflict, and Inserted is the row already in the database.
138+
// Using raw SQL condition
139+
RawWhere = "EXCLUDED.some_price > INSERTED.some_price",
140+
141+
// OR using a lambda expression
142+
Where = (inserted, excluded) => excluded.Price > inserted.Price,
143+
});
144+
```
145+
112146
## Roadmap
113147

114148
- [ ] [Add support for navigation properties](https://github.com/PhenX/PhenX.EntityFrameworkCore.BulkInsert/issues/2)

src/PhenX.EntityFrameworkCore.BulkInsert.MySql/MySqlDialectBuilder.cs

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
using System.Text;
22

3+
using Microsoft.EntityFrameworkCore;
4+
35
using PhenX.EntityFrameworkCore.BulkInsert.Dialect;
46
using PhenX.EntityFrameworkCore.BulkInsert.Metadata;
57
using PhenX.EntityFrameworkCore.BulkInsert.Options;
@@ -12,14 +14,27 @@ internal class MySqlServerDialectBuilder : SqlDialectBuilder
1214

1315
protected override string CloseDelimiter => "`";
1416

17+
/// <summary>
18+
/// Indicates whether the dialect supports moving rows from temporary table to the final table, in order to
19+
/// theoretically reduce disk space requirements.
20+
/// </summary>
1521
protected override bool SupportsMoveRows => false;
1622

23+
/// <summary>
24+
/// Indicates whether the dialect supports INSERT INTO table AS alias.
25+
/// </summary>
26+
protected override bool SupportsInsertIntoAlias => false;
27+
1728
public override string CreateTableCopySql(string tempNameName, TableMetadata tableInfo, IReadOnlyList<ColumnMetadata> columns)
1829
{
1930
return $"CREATE TEMPORARY TABLE {tempNameName} SELECT * FROM {tableInfo.QuotedTableName} WHERE 1 = 0;";
2031
}
2132

22-
protected override void AppendConflictCondition<T>(StringBuilder sql, OnConflictOptions<T> onConflictTyped)
33+
protected override void AppendConflictCondition<T>(
34+
StringBuilder sql,
35+
TableMetadata target,
36+
DbContext context,
37+
OnConflictOptions<T> onConflictTyped)
2338
{
2439
throw new NotSupportedException("Conflict conditions are not supported in MYSQL");
2540
}

src/PhenX.EntityFrameworkCore.BulkInsert.SqlServer/SqlServerDialectBuilder.cs

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
using System.Text;
22

3+
using Microsoft.EntityFrameworkCore;
4+
35
using PhenX.EntityFrameworkCore.BulkInsert.Dialect;
46
using PhenX.EntityFrameworkCore.BulkInsert.Metadata;
57
using PhenX.EntityFrameworkCore.BulkInsert.Options;
@@ -35,6 +37,7 @@ public override string CreateTableCopySql(string tempTableName, TableMetadata ta
3537
}
3638

3739
public override string BuildMoveDataSql<T>(
40+
DbContext context,
3841
TableMetadata target,
3942
string source,
4043
IReadOnlyList<ColumnMetadata> insertedColumns,
@@ -66,24 +69,31 @@ public override string BuildMoveDataSql<T>(
6669
throw new InvalidOperationException("Table has no primary key that can be used for conflict detection.");
6770
}
6871

69-
q.AppendLine($"MERGE INTO {target.QuotedTableName} AS TARGET");
72+
q.AppendLine($"MERGE INTO {target.QuotedTableName} AS INSERTED");
7073

7174
q.Append("USING (SELECT ");
7275
q.AppendColumns(insertedColumns);
73-
q.Append($" FROM {source}) AS SOURCE (");
76+
q.Append($" FROM {source}) AS EXCLUDED (");
7477
q.AppendColumns(insertedColumns);
7578
q.AppendLine(")");
7679

7780
q.Append("ON ");
78-
q.AppendJoin(" AND ", matchColumns, (b, col) => b.Append($"TARGET.{col} = SOURCE.{col}"));
81+
q.AppendJoin(" AND ", matchColumns, (b, col) => b.Append($"INSERTED.{col} = EXCLUDED.{col}"));
7982
q.AppendLine();
8083

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

85-
q.AppendLine("WHEN MATCHED THEN UPDATE SET ");
86-
q.AppendJoin(", ", GetUpdates(target, columns, onConflictTyped.Update));
88+
q.AppendLine("WHEN MATCHED ");
89+
90+
if (!string.IsNullOrEmpty(onConflictTyped.RawWhere))
91+
{
92+
q.Append($"AND {onConflictTyped.RawWhere} ");
93+
}
94+
95+
q.AppendLine("THEN UPDATE SET ");
96+
q.AppendJoin(", ", GetUpdates(context, target, columns, onConflictTyped.Update));
8797
q.AppendLine();
8898
}
8999

@@ -92,7 +102,7 @@ public override string BuildMoveDataSql<T>(
92102
q.AppendLine(")");
93103

94104
q.Append("VALUES (");
95-
q.AppendJoin(", ", insertedColumns, (b, col) => b.Append($"SOURCE.{col.QuotedColumName}"));
105+
q.AppendJoin(", ", insertedColumns, (b, col) => b.Append($"EXCLUDED.{col.QuotedColumName}"));
96106
q.AppendLine(")");
97107

98108
if (returnedColumns.Count != 0)
@@ -134,9 +144,4 @@ public override string BuildMoveDataSql<T>(
134144
var result = q.ToString();
135145
return result;
136146
}
137-
138-
protected override string GetExcludedColumnName(string columnName)
139-
{
140-
return $"SOURCE.{columnName}";
141-
}
142147
}

src/PhenX.EntityFrameworkCore.BulkInsert.Sqlite/SqliteDialectBuilder.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,9 @@ public override string CreateTableCopySql(string tempNameName, TableMetadata tab
1515
{
1616
return $"CREATE TEMP TABLE {tempNameName} AS SELECT * FROM {tableInfo.QuotedTableName} WHERE 0;";
1717
}
18+
19+
protected override string Trim(string lhs)
20+
{
21+
return $"TRIM({lhs})";
22+
}
1823
}

src/PhenX.EntityFrameworkCore.BulkInsert/BulkInsertProviderBase.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
using System.Runtime.CompilerServices;
2-
using System.Runtime.InteropServices;
32

43
using Microsoft.EntityFrameworkCore;
54
using Microsoft.EntityFrameworkCore.Storage;
@@ -202,6 +201,7 @@ protected virtual async Task AddBulkInsertIdColumn<T>(
202201
{
203202
var query =
204203
SqlDialect.BuildMoveDataSql<T>(
204+
context,
205205
tableInfo,
206206
tempTableName,
207207
tableInfo.GetColumns(options.CopyGeneratedColumns),

0 commit comments

Comments
 (0)