Skip to content

Commit 479c41f

Browse files
author
fabien.menager
committed
Merge branch 'main' into feature/oracle
2 parents 4ce4dfa + 4913279 commit 479c41f

9 files changed

Lines changed: 314 additions & 80 deletions

File tree

README.md

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ Install-Package PhenX.EntityFrameworkCore.BulkInsert.Oracle
4747

4848
## Usage
4949

50-
1. Register the bulk insert provider in your `DbContextOptions`:
50+
Register the bulk insert provider in your `DbContextOptions`:
5151

5252
```csharp
5353
services.AddDbContext<MyDbContext>(options =>
@@ -68,7 +68,7 @@ services.AddDbContext<MyDbContext>(options =>
6868
});
6969
```
7070

71-
2. Use the bulk insert extension method:
71+
### Very basic usage
7272

7373
```csharp
7474
// Asynchronously
@@ -78,7 +78,7 @@ await dbContext.ExecuteBulkInsertAsync(entities);
7878
dbContext.ExecuteBulkInsert(entities);
7979
```
8080

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

8383
```csharp
8484
// Common options
@@ -109,12 +109,47 @@ await dbContext.ExecuteBulkInsertAsync(entities, o =>
109109
});
110110
```
111111

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

114114
```csharp
115115
await dbContext.ExecuteBulkInsertReturnEntitiesAsync(entities);
116116
```
117117

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

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

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

Lines changed: 11 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,22 @@ internal class MySqlServerDialectBuilder : SqlDialectBuilder
1214

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

17+
/// <inheritdoc />
1518
protected override bool SupportsMoveRows => false;
1619

20+
/// <inheritdoc />
21+
protected override bool SupportsInsertIntoAlias => false;
22+
1723
public override string CreateTableCopySql(string tempNameName, TableMetadata tableInfo, IReadOnlyList<ColumnMetadata> columns)
1824
{
1925
return $"CREATE TEMPORARY TABLE {tempNameName} SELECT * FROM {tableInfo.QuotedTableName} WHERE 1 = 0;";
2026
}
2127

22-
protected override void AppendConflictCondition<T>(StringBuilder sql, OnConflictOptions<T> onConflictTyped)
28+
protected override void AppendConflictCondition<T>(
29+
StringBuilder sql,
30+
TableMetadata target,
31+
DbContext context,
32+
OnConflictOptions<T> onConflictTyped)
2333
{
2434
throw new NotSupportedException("Conflict conditions are not supported in MYSQL");
2535
}

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

Lines changed: 27 additions & 15 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;
@@ -34,7 +36,10 @@ public override string CreateTableCopySql(string tempTableName, TableMetadata ta
3436
return q.ToString();
3537
}
3638

39+
protected override string Trim(string lhs) => $"TRIM({lhs})";
40+
3741
public override string BuildMoveDataSql<T>(
42+
DbContext context,
3843
TableMetadata target,
3944
string source,
4045
IReadOnlyList<ColumnMetadata> insertedColumns,
@@ -66,24 +71,37 @@ public override string BuildMoveDataSql<T>(
6671
throw new InvalidOperationException("Table has no primary key that can be used for conflict detection.");
6772
}
6873

69-
q.AppendLine($"MERGE INTO {target.QuotedTableName} AS TARGET");
74+
q.AppendLine($"MERGE INTO {target.QuotedTableName} AS {PseudoTableInserted}");
7075

7176
q.Append("USING (SELECT ");
7277
q.AppendColumns(insertedColumns);
73-
q.Append($" FROM {source}) AS SOURCE (");
78+
q.Append($" FROM {source}) AS {PseudoTableExcluded} (");
7479
q.AppendColumns(insertedColumns);
7580
q.AppendLine(")");
7681

7782
q.Append("ON ");
78-
q.AppendJoin(" AND ", matchColumns, (b, col) => b.Append($"TARGET.{col} = SOURCE.{col}"));
83+
q.AppendJoin(" AND ", matchColumns, (b, col) => b.Append($"{PseudoTableInserted}.{col} = {PseudoTableExcluded}.{col}"));
7984
q.AppendLine();
8085

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

85-
q.AppendLine("WHEN MATCHED THEN UPDATE SET ");
86-
q.AppendJoin(", ", GetUpdates(target, columns, onConflictTyped.Update));
90+
q.AppendLine("WHEN MATCHED ");
91+
92+
if (onConflictTyped.RawWhere != null || onConflictTyped.Where != null)
93+
{
94+
if (onConflictTyped is { RawWhere: not null, Where: not null })
95+
{
96+
throw new ArgumentException("Cannot specify both RawWhere and Where in OnConflictOptions.");
97+
}
98+
99+
q.Append("AND ");
100+
AppendConflictCondition(q, target, context, onConflictTyped);
101+
}
102+
103+
q.AppendLine("THEN UPDATE SET ");
104+
q.AppendJoin(", ", GetUpdates(context, target, columns, onConflictTyped.Update));
87105
q.AppendLine();
88106
}
89107

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

94112
q.Append("VALUES (");
95-
q.AppendJoin(", ", insertedColumns, (b, col) => b.Append($"SOURCE.{col.QuotedColumName}"));
113+
q.AppendJoin(", ", insertedColumns, (b, col) => b.Append($"{PseudoTableExcluded}.{col.QuotedColumName}"));
96114
q.AppendLine(")");
97115

98116
if (returnedColumns.Count != 0)
99117
{
100118
q.Append("OUTPUT ");
101-
q.AppendJoin(", ", returnedColumns, (b, col) => b.Append($"INSERTED.{col.QuotedColumName} AS {col.QuotedColumName}"));
119+
q.AppendJoin(", ", returnedColumns, (b, col) => b.Append($"{PseudoTableInserted}.{col.QuotedColumName} AS {col.QuotedColumName}"));
102120
q.AppendLine();
103121
}
104122
}
@@ -113,7 +131,7 @@ public override string BuildMoveDataSql<T>(
113131
if (returnedColumns.Count != 0)
114132
{
115133
q.Append("OUTPUT ");
116-
q.AppendJoin(", ", returnedColumns, (b, col) => b.Append($"INSERTED.{col.QuotedColumName} AS {col.QuotedColumName}"));
134+
q.AppendJoin(", ", returnedColumns, (b, col) => b.Append($"{PseudoTableInserted}.{col.QuotedColumName} AS {col.QuotedColumName}"));
117135
q.AppendLine();
118136
}
119137

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

134-
var result = q.ToString();
135-
return result;
136-
}
137-
138-
protected override string GetExcludedColumnName(string columnName)
139-
{
140-
return $"SOURCE.{columnName}";
152+
return q.ToString();
141153
}
142154
}

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,6 @@ 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) => $"TRIM({lhs})";
1820
}

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;
@@ -208,6 +207,7 @@ protected virtual async Task AddBulkInsertIdColumn<T>(
208207
{
209208
var query =
210209
SqlDialect.BuildMoveDataSql<T>(
210+
context,
211211
tableInfo,
212212
tempTableName,
213213
tableInfo.GetColumns(options.CopyGeneratedColumns),

0 commit comments

Comments
 (0)