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 ;
2019-12-12 12:55:17 +01:00
using System.Data.SqlClient ;
2018-06-29 19:52:40 +02:00
using System.Linq ;
using System.Text ;
using NPoco ;
using StackExchange.Profiling ;
using Umbraco.Core.Logging ;
using Umbraco.Core.Persistence.FaultHandling ;
2019-12-12 12:55:17 +01:00
using Umbraco.Core.Persistence.SqlSyntax ;
2018-06-29 19:52:40 +02:00
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
{
private readonly ILogger _logger ;
2019-12-12 12:55:17 +01:00
private readonly IBulkSqlInsertProvider _bulkSqlInsertProvider ;
2018-06-29 19:52:40 +02:00
private readonly RetryPolicy _connectionRetryPolicy ;
private readonly RetryPolicy _commandRetryPolicy ;
private readonly Guid _instanceGuid = Guid . NewGuid ( ) ;
#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>
2019-12-12 12:55:17 +01:00
public UmbracoDatabase ( string connectionString , ISqlContext sqlContext , DbProviderFactory provider , ILogger logger , IBulkSqlInsertProvider bulkSqlInsertProvider , 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 ;
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>
2019-12-12 12:55:17 +01:00
internal UmbracoDatabase ( DbConnection connection , ISqlContext sqlContext , ILogger 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 ; }
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
}
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
{
2018-08-17 15:41:58 +01:00
_logger . Error < UmbracoDatabase > ( ex , "Exception ({InstanceId})." , InstanceId ) ;
2018-08-14 15:08:32 +01:00
_logger . Debug < UmbracoDatabase > ( "At:\r\n{StackTrace}" , Environment . StackTrace ) ;
2018-06-29 19:52:40 +02:00
if ( EnableSqlTrace = = false )
2018-08-14 15:08:32 +01:00
_logger . Debug < UmbracoDatabase > ( "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 )
2019-01-26 09:42:14 -05:00
_logger . Debug < UmbracoDatabase > ( "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 ) ;
if ( refsobj ! = null ) _logger . Debug < UmbracoDatabase > ( "Oops!" + Environment . NewLine + refsobj ) ;
#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 + + ;
base . OnExecutedCommand ( cmd ) ;
}
#endregion
}
}