2018-06-29 19:52:40 +02:00
|
|
|
|
using System;
|
2019-12-12 12:55:17 +01:00
|
|
|
|
using System.Collections.Generic;
|
2018-06-29 19:52:40 +02:00
|
|
|
|
using System.Data;
|
|
|
|
|
|
using System.Data.Common;
|
|
|
|
|
|
using System.Linq;
|
|
|
|
|
|
using System.Text;
|
2020-09-16 13:08:27 +02:00
|
|
|
|
using Microsoft.Extensions.Logging;
|
2018-06-29 19:52:40 +02:00
|
|
|
|
using NPoco;
|
|
|
|
|
|
using StackExchange.Profiling;
|
2021-02-12 12:40:08 +01:00
|
|
|
|
using Umbraco.Cms.Infrastructure.Migrations.Install;
|
2018-06-29 19:52:40 +02:00
|
|
|
|
using Umbraco.Core.Persistence.FaultHandling;
|
|
|
|
|
|
|
|
|
|
|
|
namespace Umbraco.Core.Persistence
|
|
|
|
|
|
{
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Extends NPoco Database for Umbraco.
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
/// <remarks>
|
|
|
|
|
|
/// <para>Is used everywhere in place of the original NPoco Database object, and provides additional features
|
|
|
|
|
|
/// such as profiling, retry policies, logging, etc.</para>
|
|
|
|
|
|
/// <para>Is never created directly but obtained from the <see cref="UmbracoDatabaseFactory"/>.</para>
|
|
|
|
|
|
/// </remarks>
|
|
|
|
|
|
public class UmbracoDatabase : Database, IUmbracoDatabase
|
|
|
|
|
|
{
|
2020-09-16 13:08:27 +02:00
|
|
|
|
private readonly ILogger<UmbracoDatabase> _logger;
|
2019-12-12 12:55:17 +01:00
|
|
|
|
private readonly IBulkSqlInsertProvider _bulkSqlInsertProvider;
|
2021-01-18 15:40:22 +01:00
|
|
|
|
private readonly DatabaseSchemaCreatorFactory _databaseSchemaCreatorFactory;
|
2018-06-29 19:52:40 +02:00
|
|
|
|
private readonly RetryPolicy _connectionRetryPolicy;
|
|
|
|
|
|
private readonly RetryPolicy _commandRetryPolicy;
|
|
|
|
|
|
private readonly Guid _instanceGuid = Guid.NewGuid();
|
2020-03-30 17:25:29 +11:00
|
|
|
|
private List<CommandInfo> _commands;
|
2018-06-29 19:52:40 +02:00
|
|
|
|
|
|
|
|
|
|
#region Ctor
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Initializes a new instance of the <see cref="UmbracoDatabase"/> class.
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
/// <remarks>
|
|
|
|
|
|
/// <para>Used by UmbracoDatabaseFactory to create databases.</para>
|
|
|
|
|
|
/// <para>Also used by DatabaseBuilder for creating databases and installing/upgrading.</para>
|
|
|
|
|
|
/// </remarks>
|
2021-01-18 15:40:22 +01:00
|
|
|
|
public UmbracoDatabase(string connectionString, ISqlContext sqlContext, DbProviderFactory provider, ILogger<UmbracoDatabase> logger, IBulkSqlInsertProvider bulkSqlInsertProvider, DatabaseSchemaCreatorFactory databaseSchemaCreatorFactory, RetryPolicy connectionRetryPolicy = null, RetryPolicy commandRetryPolicy = null)
|
2019-10-15 00:04:41 +11:00
|
|
|
|
: base(connectionString, sqlContext.DatabaseType, provider, sqlContext.SqlSyntax.DefaultIsolationLevel)
|
2018-06-29 19:52:40 +02:00
|
|
|
|
{
|
|
|
|
|
|
SqlContext = sqlContext;
|
|
|
|
|
|
|
|
|
|
|
|
_logger = logger;
|
2019-12-12 12:55:17 +01:00
|
|
|
|
_bulkSqlInsertProvider = bulkSqlInsertProvider;
|
2021-01-18 15:40:22 +01:00
|
|
|
|
_databaseSchemaCreatorFactory = databaseSchemaCreatorFactory;
|
2018-06-29 19:52:40 +02:00
|
|
|
|
_connectionRetryPolicy = connectionRetryPolicy;
|
|
|
|
|
|
_commandRetryPolicy = commandRetryPolicy;
|
|
|
|
|
|
|
|
|
|
|
|
EnableSqlTrace = EnableSqlTraceDefault;
|
2019-11-20 12:15:27 +11:00
|
|
|
|
|
|
|
|
|
|
NPocoDatabaseExtensions.ConfigureNPocoBulkExtensions();
|
2018-06-29 19:52:40 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Initializes a new instance of the <see cref="UmbracoDatabase"/> class.
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
/// <remarks>Internal for unit tests only.</remarks>
|
2020-09-16 13:08:27 +02:00
|
|
|
|
internal UmbracoDatabase(DbConnection connection, ISqlContext sqlContext, ILogger<UmbracoDatabase> logger, IBulkSqlInsertProvider bulkSqlInsertProvider)
|
2019-10-15 00:04:41 +11:00
|
|
|
|
: base(connection, sqlContext.DatabaseType, sqlContext.SqlSyntax.DefaultIsolationLevel)
|
2018-06-29 19:52:40 +02:00
|
|
|
|
{
|
|
|
|
|
|
SqlContext = sqlContext;
|
|
|
|
|
|
_logger = logger;
|
2019-12-12 12:55:17 +01:00
|
|
|
|
_bulkSqlInsertProvider = bulkSqlInsertProvider;
|
2018-06-29 19:52:40 +02:00
|
|
|
|
|
|
|
|
|
|
EnableSqlTrace = EnableSqlTraceDefault;
|
2019-11-20 12:15:27 +11:00
|
|
|
|
|
|
|
|
|
|
NPocoDatabaseExtensions.ConfigureNPocoBulkExtensions();
|
2018-06-29 19:52:40 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
|
|
/// <inheritdoc />
|
|
|
|
|
|
public ISqlContext SqlContext { get; }
|
|
|
|
|
|
|
2018-12-04 08:23:18 +01:00
|
|
|
|
#region Temp
|
|
|
|
|
|
|
|
|
|
|
|
// work around NPoco issue https://github.com/schotime/NPoco/issues/517 while we wait for the fix
|
|
|
|
|
|
public override DbCommand CreateCommand(DbConnection connection, CommandType commandType, string sql, params object[] args)
|
|
|
|
|
|
{
|
|
|
|
|
|
var command = base.CreateCommand(connection, commandType, sql, args);
|
|
|
|
|
|
|
|
|
|
|
|
if (!DatabaseType.IsSqlCe()) return command;
|
|
|
|
|
|
|
|
|
|
|
|
foreach (DbParameter parameter in command.Parameters)
|
|
|
|
|
|
if (parameter.Value == DBNull.Value)
|
|
|
|
|
|
parameter.DbType = DbType.String;
|
|
|
|
|
|
|
|
|
|
|
|
return command;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
2018-06-29 19:52:40 +02:00
|
|
|
|
#region Testing, Debugging and Troubleshooting
|
|
|
|
|
|
|
|
|
|
|
|
private bool _enableCount;
|
|
|
|
|
|
|
|
|
|
|
|
#if DEBUG_DATABASES
|
|
|
|
|
|
private int _spid = -1;
|
|
|
|
|
|
private const bool EnableSqlTraceDefault = true;
|
|
|
|
|
|
#else
|
|
|
|
|
|
private string _instanceId;
|
|
|
|
|
|
private const bool EnableSqlTraceDefault = false;
|
|
|
|
|
|
#endif
|
|
|
|
|
|
|
|
|
|
|
|
/// <inheritdoc />
|
|
|
|
|
|
public string InstanceId
|
|
|
|
|
|
{
|
|
|
|
|
|
get
|
|
|
|
|
|
{
|
|
|
|
|
|
#if DEBUG_DATABASES
|
|
|
|
|
|
return _instanceGuid.ToString("N").Substring(0, 8) + ':' + _spid;
|
|
|
|
|
|
#else
|
|
|
|
|
|
return _instanceId ?? (_instanceId = _instanceGuid.ToString("N").Substring(0, 8));
|
|
|
|
|
|
#endif
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <inheritdoc />
|
|
|
|
|
|
public bool InTransaction { get; private set; }
|
|
|
|
|
|
|
|
|
|
|
|
protected override void OnBeginTransaction()
|
|
|
|
|
|
{
|
|
|
|
|
|
base.OnBeginTransaction();
|
|
|
|
|
|
InTransaction = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
protected override void OnAbortTransaction()
|
|
|
|
|
|
{
|
|
|
|
|
|
InTransaction = false;
|
|
|
|
|
|
base.OnAbortTransaction();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
protected override void OnCompleteTransaction()
|
|
|
|
|
|
{
|
|
|
|
|
|
InTransaction = false;
|
|
|
|
|
|
base.OnCompleteTransaction();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Gets or sets a value indicating whether to log all executed Sql statements.
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
internal bool EnableSqlTrace { get; set; }
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Gets or sets a value indicating whether to count all executed Sql statements.
|
|
|
|
|
|
/// </summary>
|
2019-12-12 12:55:17 +01:00
|
|
|
|
public bool EnableSqlCount
|
2018-06-29 19:52:40 +02:00
|
|
|
|
{
|
|
|
|
|
|
get => _enableCount;
|
|
|
|
|
|
set
|
|
|
|
|
|
{
|
|
|
|
|
|
_enableCount = value;
|
|
|
|
|
|
if (_enableCount == false)
|
|
|
|
|
|
SqlCount = 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Gets the count of all executed Sql statements.
|
|
|
|
|
|
/// </summary>
|
2019-12-12 12:55:17 +01:00
|
|
|
|
public int SqlCount { get; private set; }
|
|
|
|
|
|
|
2020-03-30 17:25:29 +11:00
|
|
|
|
internal bool LogCommands
|
|
|
|
|
|
{
|
|
|
|
|
|
get => _commands != null;
|
|
|
|
|
|
set => _commands = value ? new List<CommandInfo>() : null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
internal IEnumerable<CommandInfo> Commands => _commands;
|
|
|
|
|
|
|
2019-12-19 17:56:48 +11:00
|
|
|
|
public int BulkInsertRecords<T>(IEnumerable<T> records)
|
2019-12-12 12:55:17 +01:00
|
|
|
|
{
|
2019-12-19 17:56:48 +11:00
|
|
|
|
return _bulkSqlInsertProvider.BulkInsertRecords(this, records);
|
2019-12-12 12:55:17 +01:00
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2021-01-18 15:40:22 +01:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Returns the <see cref="DatabaseSchemaResult"/> for the database
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public DatabaseSchemaResult ValidateSchema()
|
|
|
|
|
|
{
|
|
|
|
|
|
var dbSchema = _databaseSchemaCreatorFactory.Create(this);
|
|
|
|
|
|
var databaseSchemaValidationResult = dbSchema.ValidateSchema();
|
|
|
|
|
|
return databaseSchemaValidationResult;
|
|
|
|
|
|
}
|
2019-12-12 12:55:17 +01:00
|
|
|
|
|
2021-01-18 15:40:22 +01:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Returns true if Umbraco database tables are detected to be installed
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public bool IsUmbracoInstalled() => ValidateSchema().DetermineHasInstalledVersion();
|
2018-06-29 19:52:40 +02:00
|
|
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
|
|
#region OnSomething
|
|
|
|
|
|
|
2019-01-26 09:42:14 -05:00
|
|
|
|
// TODO: has new interceptors to replace OnSomething?
|
2018-06-29 19:52:40 +02:00
|
|
|
|
|
|
|
|
|
|
protected override DbConnection OnConnectionOpened(DbConnection connection)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (connection == null) throw new ArgumentNullException(nameof(connection));
|
|
|
|
|
|
|
|
|
|
|
|
#if DEBUG_DATABASES
|
|
|
|
|
|
// determines the database connection SPID for debugging
|
2019-01-17 12:07:31 +01:00
|
|
|
|
if (DatabaseType.IsSqlServer())
|
2018-06-29 19:52:40 +02:00
|
|
|
|
{
|
|
|
|
|
|
using (var command = connection.CreateCommand())
|
|
|
|
|
|
{
|
|
|
|
|
|
command.CommandText = "SELECT @@SPID";
|
|
|
|
|
|
_spid = Convert.ToInt32(command.ExecuteScalar());
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
// includes SqlCE
|
|
|
|
|
|
_spid = 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
#endif
|
|
|
|
|
|
|
|
|
|
|
|
// wrap the connection with a profiling connection that tracks timings
|
|
|
|
|
|
connection = new StackExchange.Profiling.Data.ProfiledDbConnection(connection, MiniProfiler.Current);
|
|
|
|
|
|
|
|
|
|
|
|
// wrap the connection with a retrying connection
|
|
|
|
|
|
if (_connectionRetryPolicy != null || _commandRetryPolicy != null)
|
|
|
|
|
|
connection = new RetryDbConnection(connection, _connectionRetryPolicy, _commandRetryPolicy);
|
|
|
|
|
|
|
|
|
|
|
|
return connection;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#if DEBUG_DATABASES
|
|
|
|
|
|
protected override void OnConnectionClosing(DbConnection conn)
|
|
|
|
|
|
{
|
|
|
|
|
|
_spid = -1;
|
|
|
|
|
|
base.OnConnectionClosing(conn);
|
|
|
|
|
|
}
|
|
|
|
|
|
#endif
|
|
|
|
|
|
|
2018-08-16 12:00:12 +01:00
|
|
|
|
protected override void OnException(Exception ex)
|
2018-06-29 19:52:40 +02:00
|
|
|
|
{
|
2020-09-16 09:40:49 +02:00
|
|
|
|
_logger.LogError(ex, "Exception ({InstanceId}).", InstanceId);
|
2020-09-16 10:24:05 +02:00
|
|
|
|
_logger.LogDebug("At:\r\n{StackTrace}", Environment.StackTrace);
|
2018-06-29 19:52:40 +02:00
|
|
|
|
if (EnableSqlTrace == false)
|
2020-09-16 10:24:05 +02:00
|
|
|
|
_logger.LogDebug("Sql:\r\n{Sql}", CommandToString(LastSQL, LastArgs));
|
2018-08-16 12:00:12 +01:00
|
|
|
|
base.OnException(ex);
|
2018-06-29 19:52:40 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private DbCommand _cmd;
|
|
|
|
|
|
|
|
|
|
|
|
protected override void OnExecutingCommand(DbCommand cmd)
|
|
|
|
|
|
{
|
|
|
|
|
|
// if no timeout is specified, and the connection has a longer timeout, use it
|
|
|
|
|
|
if (OneTimeCommandTimeout == 0 && CommandTimeout == 0 && cmd.Connection.ConnectionTimeout > 30)
|
|
|
|
|
|
cmd.CommandTimeout = cmd.Connection.ConnectionTimeout;
|
|
|
|
|
|
|
|
|
|
|
|
if (EnableSqlTrace)
|
2020-09-16 10:24:05 +02:00
|
|
|
|
_logger.LogDebug("SQL Trace:\r\n{Sql}", CommandToString(cmd).Replace("{", "{{").Replace("}", "}}")); // TODO: these escapes should be builtin
|
2018-06-29 19:52:40 +02:00
|
|
|
|
|
|
|
|
|
|
#if DEBUG_DATABASES
|
|
|
|
|
|
// detects whether the command is already in use (eg still has an open reader...)
|
|
|
|
|
|
DatabaseDebugHelper.SetCommand(cmd, InstanceId + " [T" + System.Threading.Thread.CurrentThread.ManagedThreadId + "]");
|
|
|
|
|
|
var refsobj = DatabaseDebugHelper.GetReferencedObjects(cmd.Connection);
|
2020-09-16 10:24:05 +02:00
|
|
|
|
if (refsobj != null) _logger.LogDebug("Oops!" + Environment.NewLine + refsobj);
|
2018-06-29 19:52:40 +02:00
|
|
|
|
#endif
|
|
|
|
|
|
|
|
|
|
|
|
_cmd = cmd;
|
|
|
|
|
|
base.OnExecutingCommand(cmd);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private string CommandToString(DbCommand cmd)
|
|
|
|
|
|
{
|
|
|
|
|
|
return CommandToString(cmd.CommandText, cmd.Parameters.Cast<DbParameter>().Select(x => x.Value).ToArray());
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private string CommandToString(string sql, object[] args)
|
|
|
|
|
|
{
|
2018-07-04 14:48:44 +02:00
|
|
|
|
var text = new StringBuilder();
|
2018-06-29 19:52:40 +02:00
|
|
|
|
#if DEBUG_DATABASES
|
2018-07-04 14:48:44 +02:00
|
|
|
|
text.Append(InstanceId);
|
|
|
|
|
|
text.Append(": ");
|
2018-06-29 19:52:40 +02:00
|
|
|
|
#endif
|
2018-07-04 14:48:44 +02:00
|
|
|
|
NPocoSqlExtensions.ToText(sql, args, text);
|
|
|
|
|
|
return text.ToString();
|
2018-06-29 19:52:40 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
protected override void OnExecutedCommand(DbCommand cmd)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (_enableCount)
|
|
|
|
|
|
SqlCount++;
|
|
|
|
|
|
|
2020-03-30 17:25:29 +11:00
|
|
|
|
_commands?.Add(new CommandInfo(cmd));
|
|
|
|
|
|
|
2018-06-29 19:52:40 +02:00
|
|
|
|
base.OnExecutedCommand(cmd);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#endregion
|
2020-03-30 17:25:29 +11:00
|
|
|
|
|
|
|
|
|
|
// used for tracking commands
|
|
|
|
|
|
public class CommandInfo
|
|
|
|
|
|
{
|
|
|
|
|
|
public CommandInfo(IDbCommand cmd)
|
|
|
|
|
|
{
|
|
|
|
|
|
Text = cmd.CommandText;
|
|
|
|
|
|
var parameters = new List<ParameterInfo>();
|
|
|
|
|
|
foreach (IDbDataParameter parameter in cmd.Parameters) parameters.Add(new ParameterInfo(parameter));
|
|
|
|
|
|
Parameters = parameters.ToArray();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public string Text { get; }
|
|
|
|
|
|
public ParameterInfo[] Parameters { get; }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// used for tracking commands
|
|
|
|
|
|
public class ParameterInfo
|
|
|
|
|
|
{
|
|
|
|
|
|
public ParameterInfo(IDbDataParameter parameter)
|
|
|
|
|
|
{
|
|
|
|
|
|
Name = parameter.ParameterName;
|
|
|
|
|
|
Value = parameter.Value;
|
|
|
|
|
|
DbType = parameter.DbType;
|
|
|
|
|
|
Size = parameter.Size;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public string Name { get; }
|
|
|
|
|
|
public object Value { get; }
|
|
|
|
|
|
public DbType DbType { get; }
|
|
|
|
|
|
public int Size { get; }
|
|
|
|
|
|
}
|
2018-06-29 19:52:40 +02:00
|
|
|
|
}
|
|
|
|
|
|
}
|