Skip to content

Commit e0b99cd

Browse files
committed
Cache prepared commands for the lifetime of the session.
1 parent d851b9b commit e0b99cd

File tree

5 files changed

+107
-23
lines changed

5 files changed

+107
-23
lines changed

src/MySqlConnector/Core/PreparedStatementCommandExecutor.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,17 +15,18 @@ namespace MySqlConnector.Core
1515
{
1616
internal sealed class PreparedStatementCommandExecutor : ICommandExecutor
1717
{
18-
public PreparedStatementCommandExecutor(MySqlCommand command)
18+
public PreparedStatementCommandExecutor(MySqlCommand command, PreparedStatements preparedStatements)
1919
{
2020
m_command = command;
21+
m_preparedStatements = preparedStatements;
2122
}
2223

2324
public async Task<DbDataReader> ExecuteReaderAsync(string commandText, MySqlParameterCollection parameterCollection, CommandBehavior behavior, IOBehavior ioBehavior, CancellationToken cancellationToken)
2425
{
2526
cancellationToken.ThrowIfCancellationRequested();
2627
if (Log.IsDebugEnabled())
2728
Log.Debug("Session{0} ExecuteBehavior {1} CommandText: {2}", m_command.Connection.Session.Id, ioBehavior, commandText);
28-
using (var payload = CreateQueryPayload(m_command.PreparedStatements[0], parameterCollection, m_command.Connection.GuidFormat))
29+
using (var payload = CreateQueryPayload(m_preparedStatements.Statements[0], parameterCollection, m_command.Connection.GuidFormat))
2930
using (m_command.RegisterCancel(cancellationToken))
3031
{
3132
m_command.Connection.Session.StartQuerying(m_command);
@@ -105,5 +106,6 @@ private PayloadData CreateQueryPayload(PreparedStatement preparedStatement, MySq
105106
static IMySqlConnectorLogger Log { get; } = MySqlConnectorLogManager.CreateLogger(nameof(PreparedStatementCommandExecutor));
106107

107108
readonly MySqlCommand m_command;
109+
readonly PreparedStatements m_preparedStatements;
108110
}
109111
}

src/MySqlConnector/Core/ServerSession.cs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System;
22
using System.Buffers.Text;
3+
using System.Collections.Generic;
34
using System.Data;
45
using System.Diagnostics;
56
using System.Globalization;
@@ -126,6 +127,23 @@ public void AbortCancel(MySqlCommand command)
126127
}
127128
}
128129

130+
public void AddPreparedStatement(string commandText, PreparedStatements preparedStatements)
131+
{
132+
if (m_preparedStatements == null)
133+
m_preparedStatements = new Dictionary<string, PreparedStatements>();
134+
m_preparedStatements.Add(commandText, preparedStatements);
135+
}
136+
137+
public PreparedStatements TryGetPreparedStatement(string commandText)
138+
{
139+
if (m_preparedStatements != null)
140+
{
141+
if (m_preparedStatements.TryGetValue(commandText, out var statement))
142+
return statement;
143+
}
144+
return null;
145+
}
146+
129147
public void StartQuerying(MySqlCommand command)
130148
{
131149
lock (m_lock)
@@ -218,6 +236,8 @@ public async Task DisposeAsync(IOBehavior ioBehavior, CancellationToken cancella
218236
}
219237
}
220238

239+
ClearPreparedStatements();
240+
221241
ShutdownSocket();
222242
lock (m_lock)
223243
m_state = State.Closed;
@@ -367,6 +387,9 @@ public async Task<bool> TryResetConnectionAsync(ConnectionSettings cs, IOBehavio
367387

368388
try
369389
{
390+
// clear all prepared statements; resetting the connection will clear them on the server
391+
ClearPreparedStatements();
392+
370393
if (DatabaseOverride == null && ServerVersion.Version.CompareTo(ServerVersions.SupportsResetConnection) >= 0)
371394
{
372395
m_logArguments[1] = ServerVersion.OriginalString;
@@ -1322,6 +1345,16 @@ private Exception CreateExceptionForErrorPayload(PayloadData payload)
13221345
return exception;
13231346
}
13241347

1348+
private void ClearPreparedStatements()
1349+
{
1350+
if (m_preparedStatements != null)
1351+
{
1352+
foreach (var pair in m_preparedStatements)
1353+
pair.Value.Dispose();
1354+
m_preparedStatements.Clear();
1355+
}
1356+
}
1357+
13251358
private enum State
13261359
{
13271360
// The session has been created; no connection has been made.
@@ -1372,5 +1405,6 @@ private enum State
13721405
bool m_isSecureConnection;
13731406
bool m_supportsConnectionAttributes;
13741407
bool m_supportsDeprecateEof;
1408+
Dictionary<string, PreparedStatements> m_preparedStatements;
13751409
}
13761410
}

src/MySqlConnector/MySql.Data.MySqlClient/MySqlCommand.cs

Lines changed: 10 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,10 @@ private async Task PrepareAsync(IOBehavior ioBehavior, CancellationToken cancell
8585
if (CommandType != CommandType.Text)
8686
throw new NotSupportedException("Only CommandType.Text is currently supported by MySqlCommand.Prepare");
8787

88+
// don't prepare the same SQL twice
89+
if (m_connection.Session.TryGetPreparedStatement(CommandText) != null)
90+
return;
91+
8892
var statementPreparer = new StatementPreparer(CommandText, Parameters, CreateStatementPreparerOptions());
8993
var parsedStatements = statementPreparer.SplitStatements();
9094

@@ -142,8 +146,7 @@ private async Task PrepareAsync(IOBehavior ioBehavior, CancellationToken cancell
142146
preparedStatements.Add(new PreparedStatement(response.StatementId, statement, columns, parameters));
143147
}
144148

145-
m_parsedStatements = parsedStatements;
146-
m_statements = preparedStatements;
149+
m_connection.Session.AddPreparedStatement(CommandText, new PreparedStatements(preparedStatements, parsedStatements));
147150
}
148151

149152
public override string CommandText
@@ -154,11 +157,10 @@ public override string CommandText
154157
if (m_connection?.HasActiveReader ?? false)
155158
throw new InvalidOperationException("Cannot set MySqlCommand.CommandText when there is an open DataReader for this command; it must be closed first.");
156159
m_commandText = value;
157-
ClearPreparedStatements();
158160
}
159161
}
160162

161-
public bool IsPrepared => m_statements != null;
163+
public bool IsPrepared => TryGetPreparedStatement() != null;
162164

163165
public new MySqlTransaction Transaction { get; set; }
164166

@@ -170,7 +172,6 @@ public override string CommandText
170172
if (m_connection?.HasActiveReader ?? false)
171173
throw new InvalidOperationException("Cannot set MySqlCommand.Connection when there is an open DataReader for this command; it must be closed first.");
172174
m_connection = value;
173-
ClearPreparedStatements();
174175
}
175176
}
176177

@@ -188,7 +189,6 @@ public override CommandType CommandType
188189
if (value != CommandType.Text && value != CommandType.StoredProcedure)
189190
throw new ArgumentException("CommandType must be Text or StoredProcedure.", nameof(value));
190191
m_commandType = value;
191-
ClearPreparedStatements();
192192
}
193193
}
194194

@@ -277,8 +277,9 @@ internal Task<DbDataReader> ExecuteReaderAsync(CommandBehavior behavior, IOBehav
277277
if (!IsValid(out var exception))
278278
return Utility.TaskFromException<DbDataReader>(exception);
279279

280-
if (m_statements != null)
281-
m_commandExecutor = new PreparedStatementCommandExecutor(this);
280+
var preparedStatements = TryGetPreparedStatement();
281+
if (preparedStatements != null)
282+
m_commandExecutor = new PreparedStatementCommandExecutor(this, preparedStatements);
282283
else if (m_commandType == CommandType.Text)
283284
m_commandExecutor = new TextCommandExecutor(this);
284285
else if (m_commandType == CommandType.StoredProcedure)
@@ -292,10 +293,7 @@ protected override void Dispose(bool disposing)
292293
try
293294
{
294295
if (disposing)
295-
{
296296
m_parameterCollection = null;
297-
ClearPreparedStatements();
298-
}
299297
}
300298
finally
301299
{
@@ -324,8 +322,6 @@ internal IDisposable RegisterCancel(CancellationToken token)
324322

325323
internal int CancelAttemptCount { get; set; }
326324

327-
internal IReadOnlyList<PreparedStatement> PreparedStatements => m_statements;
328-
329325
/// <summary>
330326
/// Causes the effective command timeout to be reset back to the value specified by <see cref="CommandTimeout"/>.
331327
/// </summary>
@@ -400,12 +396,7 @@ private bool IsValid(out Exception exception)
400396
return exception == null;
401397
}
402398

403-
private void ClearPreparedStatements()
404-
{
405-
m_parsedStatements?.Dispose();
406-
m_parsedStatements = null;
407-
m_statements = null;
408-
}
399+
private PreparedStatements TryGetPreparedStatement() => CommandType == CommandType.Text && !string.IsNullOrWhiteSpace(CommandText) ? m_connection.Session.TryGetPreparedStatement(CommandText) : null;
409400

410401
internal void ReaderClosed() => (m_commandExecutor as StoredProcedureCommandExecutor)?.SetParams();
411402

@@ -414,8 +405,6 @@ private void ClearPreparedStatements()
414405
MySqlConnection m_connection;
415406
string m_commandText;
416407
MySqlParameterCollection m_parameterCollection;
417-
ParsedStatements m_parsedStatements;
418-
IReadOnlyList<PreparedStatement> m_statements;
419408
int? m_commandTimeout;
420409
CommandType m_commandType;
421410
ICommandExecutor m_commandExecutor;
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
using System;
2+
using System.Collections.Generic;
3+
4+
namespace MySqlConnector.Core
5+
{
6+
internal sealed class PreparedStatements : IDisposable
7+
{
8+
public IReadOnlyList<PreparedStatement> Statements { get; }
9+
10+
public PreparedStatements(IReadOnlyList<PreparedStatement> preparedStatements, ParsedStatements parsedStatements)
11+
{
12+
Statements = preparedStatements;
13+
m_parsedStatements = parsedStatements;
14+
}
15+
16+
public void Dispose()
17+
{
18+
m_parsedStatements?.Dispose();
19+
m_parsedStatements = null;
20+
}
21+
22+
ParsedStatements m_parsedStatements;
23+
}
24+
}

tests/SideBySide/PreparedCommandTests.cs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,41 @@ public void InsertAndQuery(bool isPrepared, string dataType, object dataValue)
131131
}
132132
}
133133

134+
[Fact]
135+
public void PrepareMultipleTimes()
136+
{
137+
using (var connection = CreatePrepareConnection())
138+
{
139+
using (var cmd = new MySqlCommand("SELECT 'test';", connection))
140+
{
141+
Assert.False(cmd.IsPrepared);
142+
cmd.Prepare();
143+
Assert.True(cmd.IsPrepared);
144+
cmd.Prepare();
145+
Assert.Equal("test", cmd.ExecuteScalar());
146+
}
147+
}
148+
}
149+
150+
[SkippableFact(Baseline = "Connector/NET doesn't cache prepared commands")]
151+
public void PreparedCommandIsCached()
152+
{
153+
using (var connection = CreatePrepareConnection())
154+
{
155+
using (var cmd = new MySqlCommand("SELECT 'test';", connection))
156+
{
157+
cmd.Prepare();
158+
Assert.Equal("test", cmd.ExecuteScalar());
159+
}
160+
161+
using (var cmd = new MySqlCommand("SELECT 'test';", connection))
162+
{
163+
Assert.True(cmd.IsPrepared);
164+
Assert.Equal("test", cmd.ExecuteScalar());
165+
}
166+
}
167+
}
168+
134169
public static IEnumerable<object[]> GetInsertAndQueryData()
135170
{
136171
foreach (var isPrepared in new[] { false, true })

0 commit comments

Comments
 (0)