Skip to content

Commit 9241bd2

Browse files
author
fabien.menager
committed
Begin support for Oracle
1 parent 21c60ba commit 9241bd2

17 files changed

Lines changed: 469 additions & 5 deletions

PhenX.EntityFrameworkCore.BulkInsert.sln

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "root", "root", "{45366E91-4
3535
EndProject
3636
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PhenX.EntityFrameworkCore.BulkInsert.MySql", "src\PhenX.EntityFrameworkCore.BulkInsert.MySql\PhenX.EntityFrameworkCore.BulkInsert.MySql.csproj", "{17649766-EA68-4333-8DA8-47B014A8B2CC}"
3737
EndProject
38+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PhenX.EntityFrameworkCore.BulkInsert.Oracle", "src\PhenX.EntityFrameworkCore.BulkInsert.Oracle\PhenX.EntityFrameworkCore.BulkInsert.Oracle.csproj", "{98CC5F0A-5739-4570-A384-A3A067D09755}"
39+
EndProject
3840
Global
3941
GlobalSection(SolutionConfigurationPlatforms) = preSolution
4042
Debug|Any CPU = Debug|Any CPU
@@ -69,6 +71,10 @@ Global
6971
{17649766-EA68-4333-8DA8-47B014A8B2CC}.Debug|Any CPU.Build.0 = Debug|Any CPU
7072
{17649766-EA68-4333-8DA8-47B014A8B2CC}.Release|Any CPU.ActiveCfg = Release|Any CPU
7173
{17649766-EA68-4333-8DA8-47B014A8B2CC}.Release|Any CPU.Build.0 = Release|Any CPU
74+
{98CC5F0A-5739-4570-A384-A3A067D09755}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
75+
{98CC5F0A-5739-4570-A384-A3A067D09755}.Debug|Any CPU.Build.0 = Debug|Any CPU
76+
{98CC5F0A-5739-4570-A384-A3A067D09755}.Release|Any CPU.ActiveCfg = Release|Any CPU
77+
{98CC5F0A-5739-4570-A384-A3A067D09755}.Release|Any CPU.Build.0 = Release|Any CPU
7278
EndGlobalSection
7379
GlobalSection(SolutionProperties) = preSolution
7480
HideSolutionNode = FALSE
@@ -81,5 +87,6 @@ Global
8187
{E4EB1C53-575C-45F8-924A-93DC42E8ACCA} = {F8A83782-311C-454D-8B97-B3FB86478BF4}
8288
{450E859C-411F-4D67-A0B4-4E02C3D30E14} = {CBEBA2A8-79E0-412E-93C1-C88F4473D78B}
8389
{17649766-EA68-4333-8DA8-47B014A8B2CC} = {CBEBA2A8-79E0-412E-93C1-C88F4473D78B}
90+
{98CC5F0A-5739-4570-A384-A3A067D09755} = {CBEBA2A8-79E0-412E-93C1-C88F4473D78B}
8491
EndGlobalSection
8592
EndGlobal
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
using Oracle.ManagedDataAccess.Client;
2+
3+
using PhenX.EntityFrameworkCore.BulkInsert.Options;
4+
5+
namespace PhenX.EntityFrameworkCore.BulkInsert.Oracle;
6+
7+
/// <summary>
8+
/// Options specific to Oracle bulk insert.
9+
/// </summary>
10+
public class OracleBulkInsertOptions : BulkInsertOptions
11+
{
12+
/// <inheritdoc cref="OracleBulkCopyOptions"/>
13+
public OracleBulkCopyOptions CopyOptions { get; set; } = OracleBulkCopyOptions.Default;
14+
15+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
using JetBrains.Annotations;
2+
3+
using Microsoft.EntityFrameworkCore;
4+
using Microsoft.Extensions.Logging;
5+
6+
using Oracle.ManagedDataAccess.Client;
7+
8+
using PhenX.EntityFrameworkCore.BulkInsert.Metadata;
9+
10+
namespace PhenX.EntityFrameworkCore.BulkInsert.Oracle;
11+
12+
[UsedImplicitly]
13+
internal class OracleBulkInsertProvider(ILogger<OracleBulkInsertProvider>? logger) : BulkInsertProviderBase<OracleDialectBuilder, OracleBulkInsertOptions>(logger)
14+
{
15+
//language=sql
16+
/// <inheritdoc />
17+
protected override string AddTableCopyBulkInsertId => $"ALTER TABLE {{0}} ADD {BulkInsertId} INT IDENTITY PRIMARY KEY;";
18+
19+
/// <inheritdoc />
20+
protected override string GetTempTableName(string tableName) => $"#_temp_bulk_insert_{tableName}";
21+
22+
protected override OracleBulkInsertOptions CreateDefaultOptions() => new()
23+
{
24+
BatchSize = 50_000,
25+
Converters = [OracleGeometryConverter.Instance]
26+
};
27+
28+
/// <inheritdoc />
29+
protected override Task BulkInsert<T>(
30+
bool sync,
31+
DbContext context,
32+
TableMetadata tableInfo,
33+
IEnumerable<T> entities,
34+
string tableName,
35+
IReadOnlyList<ColumnMetadata> columns,
36+
OracleBulkInsertOptions options,
37+
CancellationToken ctk)
38+
{
39+
var connection = (OracleConnection) context.Database.GetDbConnection();
40+
41+
using var bulkCopy = new OracleBulkCopy(connection, options.CopyOptions);
42+
43+
bulkCopy.DestinationTableName = tableName;
44+
bulkCopy.BatchSize = options.BatchSize;
45+
bulkCopy.BulkCopyTimeout = options.GetCopyTimeoutInSeconds();
46+
47+
foreach (var column in columns)
48+
{
49+
bulkCopy.ColumnMappings.Add(column.PropertyName, column.QuotedColumName);
50+
}
51+
52+
var dataReader = new EnumerableDataReader<T>(entities, columns, options.Converters);
53+
54+
bulkCopy.WriteToServer(dataReader);
55+
56+
return Task.CompletedTask;
57+
}
58+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
using Microsoft.EntityFrameworkCore;
2+
3+
using PhenX.EntityFrameworkCore.BulkInsert.Extensions;
4+
5+
namespace PhenX.EntityFrameworkCore.BulkInsert.Oracle;
6+
7+
/// <summary>
8+
/// DbContext options extension for Oracle.
9+
/// </summary>
10+
public static class OracleDbContextOptionsExtensions
11+
{
12+
/// <summary>
13+
/// Configures the DbContext to use the Oracle bulk insert provider.
14+
/// </summary>
15+
public static DbContextOptionsBuilder UseBulkInsertOracle(this DbContextOptionsBuilder optionsBuilder)
16+
{
17+
return optionsBuilder.UseProvider<OracleBulkInsertProvider>();
18+
}
19+
}
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
using System.Text;
2+
3+
using PhenX.EntityFrameworkCore.BulkInsert.Dialect;
4+
using PhenX.EntityFrameworkCore.BulkInsert.Metadata;
5+
using PhenX.EntityFrameworkCore.BulkInsert.Options;
6+
7+
namespace PhenX.EntityFrameworkCore.BulkInsert.Oracle;
8+
9+
internal class OracleDialectBuilder : SqlDialectBuilder
10+
{
11+
protected override string OpenDelimiter => "\"";
12+
protected override string CloseDelimiter => "\"";
13+
protected override string ConcatOperator => "||";
14+
15+
protected override bool SupportsMoveRows => false;
16+
17+
public override string CreateTableCopySql(string tempTableName, TableMetadata tableInfo, IReadOnlyList<ColumnMetadata> columns)
18+
{
19+
var q = new StringBuilder();
20+
q.Append($"CREATE TABLE {tempTableName} (");
21+
22+
foreach (var column in columns)
23+
{
24+
q.Append($"{column.QuotedColumName} {column.StoreDefinition}");
25+
if (column != columns[^1])
26+
{
27+
q.Append(',');
28+
}
29+
q.AppendLine();
30+
}
31+
32+
q.AppendLine(")");
33+
34+
return q.ToString();
35+
}
36+
37+
public override string BuildMoveDataSql<T>(
38+
TableMetadata target,
39+
string source,
40+
IReadOnlyList<ColumnMetadata> insertedColumns,
41+
IReadOnlyList<ColumnMetadata> returnedColumns,
42+
BulkInsertOptions options,
43+
OnConflictOptions? onConflict = null)
44+
{
45+
var q = new StringBuilder();
46+
47+
if (options.CopyGeneratedColumns)
48+
{
49+
q.AppendLine($"SET IDENTITY_INSERT {target.QuotedTableName} ON;");
50+
}
51+
52+
// Merge handling
53+
if (onConflict is OnConflictOptions<T> onConflictTyped)
54+
{
55+
IEnumerable<string> matchColumns;
56+
if (onConflictTyped.Match != null)
57+
{
58+
matchColumns = GetColumns(target, onConflictTyped.Match);
59+
}
60+
else if (target.PrimaryKey.Count > 0)
61+
{
62+
matchColumns = target.PrimaryKey.Select(x => x.QuotedColumName);
63+
}
64+
else
65+
{
66+
throw new InvalidOperationException("Table has no primary key that can be used for conflict detection.");
67+
}
68+
69+
q.AppendLine($"MERGE INTO {target.QuotedTableName} AS TARGET");
70+
71+
q.Append("USING (SELECT ");
72+
q.AppendColumns(insertedColumns);
73+
q.Append($" FROM {source}) AS SOURCE (");
74+
q.AppendColumns(insertedColumns);
75+
q.AppendLine(")");
76+
77+
q.Append("ON ");
78+
q.AppendJoin(" AND ", matchColumns, (b, col) => b.Append($"TARGET.{col} = SOURCE.{col}"));
79+
q.AppendLine();
80+
81+
if (onConflictTyped.Update != null)
82+
{
83+
var columns = target.GetColumns(false);
84+
85+
q.AppendLine("WHEN MATCHED THEN UPDATE SET ");
86+
q.AppendJoin(", ", GetUpdates(target, columns, onConflictTyped.Update));
87+
q.AppendLine();
88+
}
89+
90+
q.Append("WHEN NOT MATCHED THEN INSERT (");
91+
q.AppendColumns(insertedColumns);
92+
q.AppendLine(")");
93+
94+
q.Append("VALUES (");
95+
q.AppendJoin(", ", insertedColumns, (b, col) => b.Append($"SOURCE.{col.QuotedColumName}"));
96+
q.AppendLine(")");
97+
98+
if (returnedColumns.Count != 0)
99+
{
100+
q.Append("OUTPUT ");
101+
q.AppendJoin(", ", returnedColumns, (b, col) => b.Append($"INSERTED.{col.QuotedColumName} AS {col.QuotedColumName}"));
102+
q.AppendLine();
103+
}
104+
}
105+
106+
// No conflict handling
107+
else
108+
{
109+
q.Append($"INSERT INTO {target.QuotedTableName} (");
110+
q.AppendColumns(insertedColumns);
111+
q.AppendLine(")");
112+
113+
if (returnedColumns.Count != 0)
114+
{
115+
q.Append("OUTPUT ");
116+
q.AppendJoin(", ", returnedColumns, (b, col) => b.Append($"INSERTED.{col.QuotedColumName} AS {col.QuotedColumName}"));
117+
q.AppendLine();
118+
}
119+
120+
q.Append("SELECT ");
121+
q.AppendColumns(insertedColumns);
122+
q.AppendLine();
123+
q.Append($"FROM {source}");
124+
q.AppendLine();
125+
}
126+
127+
q.AppendLine(";");
128+
129+
if (options.CopyGeneratedColumns)
130+
{
131+
q.AppendLine($"SET IDENTITY_INSERT {target.QuotedTableName} OFF;");
132+
}
133+
134+
var result = q.ToString();
135+
return result;
136+
}
137+
138+
protected override string GetExcludedColumnName(string columnName)
139+
{
140+
return $"SOURCE.{columnName}";
141+
}
142+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
using NetTopologySuite.Geometries;
2+
3+
using PhenX.EntityFrameworkCore.BulkInsert.Abstractions;
4+
5+
namespace PhenX.EntityFrameworkCore.BulkInsert.Oracle;
6+
7+
internal sealed class OracleGeometryConverter : IBulkValueConverter
8+
{
9+
public static readonly OracleGeometryConverter Instance = new();
10+
11+
private OracleGeometryConverter()
12+
{
13+
}
14+
15+
public bool TryConvertValue(object source, out object result)
16+
{
17+
if (source is Geometry geometry)
18+
{
19+
// result = SqlGeometry.STGeomFromWKB(new SqlBytes(reversed.AsBinary()), geometry.SRID);
20+
result = null!;
21+
return true;
22+
}
23+
24+
result = source;
25+
return false;
26+
}
27+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<ItemGroup>
4+
<ProjectReference Include="..\PhenX.EntityFrameworkCore.BulkInsert\PhenX.EntityFrameworkCore.BulkInsert.csproj" />
5+
</ItemGroup>
6+
7+
<ItemGroup>
8+
<PackageReference Include="NetTopologySuite" Version="2.6.0" />
9+
<PackageReference Include="Oracle.EntityFrameworkCore" Condition="'$(TargetFramework)' == 'net8.0'" Version="8.23.80" />
10+
<PackageReference Include="Oracle.EntityFrameworkCore" Condition="'$(TargetFramework)' == 'net9.0'" Version="9.23.80" />
11+
</ItemGroup>
12+
13+
</Project>

src/PhenX.EntityFrameworkCore.BulkInsert/PhenX.EntityFrameworkCore.BulkInsert.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
<InternalsVisibleTo Include="PhenX.EntityFrameworkCore.BulkInsert.PostgreSql" />
1212
<InternalsVisibleTo Include="PhenX.EntityFrameworkCore.BulkInsert.SqlServer" />
1313
<InternalsVisibleTo Include="PhenX.EntityFrameworkCore.BulkInsert.Sqlite" />
14+
<InternalsVisibleTo Include="PhenX.EntityFrameworkCore.BulkInsert.Oracle" />
1415
</ItemGroup>
1516

1617
</Project>

tests/PhenX.EntityFrameworkCore.BulkInsert.Benchmark/LibComparator.RawInsert.cs

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88

99
using Npgsql;
1010

11+
using Oracle.ManagedDataAccess.Client;
12+
1113
namespace PhenX.EntityFrameworkCore.BulkInsert.Benchmark;
1214

1315
public abstract partial class LibComparator
@@ -194,4 +196,57 @@ private void RawInsertMySql()
194196
bulkCopy.WriteToServer(dataTable);
195197
}
196198
}
199+
200+
private void RawInsertOracle()
201+
{
202+
var connection = (OracleConnection)DbContext.Database.GetDbConnection();
203+
if (connection.State != ConnectionState.Open)
204+
{
205+
connection.Open();
206+
}
207+
208+
using var bulkCopy = new OracleBulkCopy(connection);
209+
210+
bulkCopy.DestinationTableName = "\"" + nameof(TestEntity) + "\"";
211+
bulkCopy.BatchSize = 50_000;
212+
bulkCopy.BulkCopyTimeout = 60;
213+
214+
bulkCopy.ColumnMappings.Add("Name", "\"Name\"");
215+
bulkCopy.ColumnMappings.Add("Price", "\"Price\"");
216+
bulkCopy.ColumnMappings.Add("Identifier", "\"Identifier\"");
217+
bulkCopy.ColumnMappings.Add("CreatedAt", "\"CreatedAt\"");
218+
bulkCopy.ColumnMappings.Add("UpdatedAt", "\"UpdatedAt\"");
219+
bulkCopy.ColumnMappings.Add("NumericEnumValue", "\"NumericEnumValue\"");
220+
221+
var dataTable = new DataTable();
222+
dataTable.Columns.Add("Name", typeof(string));
223+
dataTable.Columns.Add("Price", typeof(decimal));
224+
dataTable.Columns.Add("Identifier", typeof(Guid));
225+
dataTable.Columns.Add("CreatedAt", typeof(DateTime));
226+
dataTable.Columns.Add("UpdatedAt", typeof(DateTimeOffset));
227+
dataTable.Columns.Add("NumericEnumValue", typeof(int));
228+
229+
foreach (var entity in data)
230+
{
231+
var row = dataTable.NewRow();
232+
row["Name"] = entity.Name;
233+
row["Price"] = entity.Price;
234+
row["Identifier"] = entity.Identifier;
235+
row["CreatedAt"] = entity.CreatedAt;
236+
row["UpdatedAt"] = entity.UpdatedAt;
237+
row["NumericEnumValue"] = (int)entity.NumericEnumValue;
238+
dataTable.Rows.Add(row);
239+
240+
if (dataTable.Rows.Count >= 50_000)
241+
{
242+
bulkCopy.WriteToServer(dataTable);
243+
dataTable.Clear();
244+
}
245+
}
246+
247+
if (dataTable.Rows.Count > 0)
248+
{
249+
bulkCopy.WriteToServer(dataTable);
250+
}
251+
}
197252
}

tests/PhenX.EntityFrameworkCore.BulkInsert.Benchmark/LibComparator.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,11 @@ public void RawInsert()
8989
// Use MySqlBulkCopy for PostgreSQL
9090
RawInsertMySql();
9191
}
92+
else if (DbContext.Database.ProviderName!.Contains("Oracle", StringComparison.InvariantCultureIgnoreCase))
93+
{
94+
// Use OracleBulkCopy for Oracle
95+
RawInsertOracle();
96+
}
9297
}
9398

9499
[Benchmark]

0 commit comments

Comments
 (0)