PageRenderTime 50ms CodeModel.GetById 21ms RepoModel.GetById 0ms app.codeStats 1ms

/Raven.Database/Bundles/SqlReplication/RelationalDatabaseWriter.cs

https://github.com/kairogyn/ravendb
C# | 370 lines | 327 code | 41 blank | 2 comment | 27 complexity | 7f0fa28856ac2215df7f7fc52797abc9 MD5 | raw file
Possible License(s): MPL-2.0-no-copyleft-exception, BSD-3-Clause, CC-BY-SA-3.0
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Data.Common;
  4. using System.Diagnostics;
  5. using System.Globalization;
  6. using System.Reflection;
  7. using System.Text;
  8. using Raven.Abstractions;
  9. using Raven.Abstractions.Data;
  10. using Raven.Abstractions.Logging;
  11. using Raven.Database.Extensions;
  12. using Raven.Database.Indexing;
  13. using Raven.Imports.Newtonsoft.Json.Linq;
  14. using Raven.Json.Linq;
  15. using System.Linq;
  16. namespace Raven.Database.Bundles.SqlReplication
  17. {
  18. public class RelationalDatabaseWriter : IDisposable
  19. {
  20. private readonly DocumentDatabase database;
  21. private readonly SqlReplicationConfig cfg;
  22. private readonly DbProviderFactory providerFactory;
  23. private readonly SqlReplicationStatistics replicationStatistics;
  24. private readonly DbCommandBuilder commandBuilder;
  25. private readonly DbConnection connection;
  26. private readonly DbTransaction tx;
  27. private readonly List<Func<DbParameter, String, Boolean>> stringParserList;
  28. private static readonly ILog log = LogManager.GetCurrentClassLogger();
  29. bool hadErrors;
  30. public RelationalDatabaseWriter( DocumentDatabase database, SqlReplicationConfig cfg, SqlReplicationStatistics replicationStatistics)
  31. {
  32. this.database = database;
  33. this.cfg = cfg;
  34. this.replicationStatistics = replicationStatistics;
  35. providerFactory = GetDbProviderFactory(cfg);
  36. commandBuilder = providerFactory.CreateCommandBuilder();
  37. connection = providerFactory.CreateConnection();
  38. Debug.Assert(connection != null);
  39. Debug.Assert(commandBuilder != null);
  40. connection.ConnectionString = cfg.ConnectionString;
  41. try
  42. {
  43. connection.Open();
  44. }
  45. catch (Exception e)
  46. {
  47. database.AddAlert(new Alert
  48. {
  49. AlertLevel = AlertLevel.Error,
  50. CreatedAt = SystemTime.UtcNow,
  51. Exception = e.ToString(),
  52. Title = "Sql Replication could not open connection",
  53. Message = "Sql Replication could not open connection to " + connection.ConnectionString,
  54. UniqueKey = "Sql Replication Connection Error: " + connection.ConnectionString
  55. });
  56. throw;
  57. }
  58. tx = connection.BeginTransaction();
  59. stringParserList = new List<Func<DbParameter, string, bool>> {
  60. (colParam, value) => {
  61. if( char.IsDigit( value[ 0 ] ) ) {
  62. DateTime dateTime;
  63. if (DateTime.TryParseExact(value, Default.OnlyDateTimeFormat, CultureInfo.InvariantCulture,
  64. DateTimeStyles.RoundtripKind, out dateTime))
  65. {
  66. switch( providerFactory.GetType( ).Name ) {
  67. case "MySqlClientFactory":
  68. colParam.Value = dateTime.ToString( "yyyy-MM-dd HH:mm:ss.ffffff" );
  69. break;
  70. default:
  71. colParam.Value = dateTime;
  72. break;
  73. }
  74. return true;
  75. }
  76. }
  77. return false;
  78. },
  79. (colParam, value) => {
  80. if( char.IsDigit( value[ 0 ] ) ) {
  81. DateTimeOffset dateTimeOffset;
  82. if( DateTimeOffset.TryParseExact( value, Default.DateTimeFormatsToRead, CultureInfo.InvariantCulture,
  83. DateTimeStyles.RoundtripKind, out dateTimeOffset ) ) {
  84. switch( providerFactory.GetType( ).Name ) {
  85. case "MySqlClientFactory":
  86. colParam.Value = dateTimeOffset.ToUniversalTime().ToString( "yyyy-MM-dd HH:mm:ss.ffffff" );
  87. break;
  88. default:
  89. colParam.Value = dateTimeOffset;
  90. break;
  91. }
  92. return true;
  93. }
  94. }
  95. return false;
  96. }
  97. };
  98. }
  99. public bool Execute(ConversionScriptResult scriptResult)
  100. {
  101. var identifiers = scriptResult.Data.SelectMany(x => x.Value).Select(x => x.DocumentId).Distinct().ToList();
  102. foreach (var sqlReplicationTable in cfg.SqlReplicationTables)
  103. {
  104. // first, delete all the rows that might already exist there
  105. DeleteItems(sqlReplicationTable.TableName, sqlReplicationTable.DocumentKeyColumn, cfg.ParameterizeDeletesDisabled,
  106. identifiers);
  107. }
  108. foreach (var sqlReplicationTable in cfg.SqlReplicationTables)
  109. {
  110. List<ItemToReplicate> dataForTable;
  111. if (scriptResult.Data.TryGetValue(sqlReplicationTable.TableName, out dataForTable) == false)
  112. continue;
  113. InsertItems(sqlReplicationTable.TableName, sqlReplicationTable.DocumentKeyColumn, dataForTable);
  114. }
  115. Commit();
  116. return hadErrors == false;
  117. }
  118. public bool Commit()
  119. {
  120. tx.Commit();
  121. return true;
  122. }
  123. private void InsertItems(string tableName, string pkName, List<ItemToReplicate> dataForTable)
  124. {
  125. foreach (var itemToReplicate in dataForTable)
  126. {
  127. using (var cmd = connection.CreateCommand())
  128. {
  129. cmd.Transaction = tx;
  130. database.WorkContext.CancellationToken.ThrowIfCancellationRequested();
  131. var sb = new StringBuilder("INSERT INTO ")
  132. .Append(commandBuilder.QuoteIdentifier(tableName))
  133. .Append(" (")
  134. .Append(commandBuilder.QuoteIdentifier(pkName))
  135. .Append(", ");
  136. foreach (var column in itemToReplicate.Columns)
  137. {
  138. if (column.Key == pkName)
  139. continue;
  140. sb.Append(commandBuilder.QuoteIdentifier(column.Key)).Append(", ");
  141. }
  142. sb.Length = sb.Length - 2;
  143. var pkParam = cmd.CreateParameter();
  144. pkParam.ParameterName = GetParameterName(providerFactory, commandBuilder, pkName);
  145. pkParam.Value = itemToReplicate.DocumentId;
  146. cmd.Parameters.Add(pkParam);
  147. sb.Append(") \r\nVALUES (")
  148. .Append(GetParameterName(providerFactory, commandBuilder, pkName))
  149. .Append(", ");
  150. foreach (var column in itemToReplicate.Columns)
  151. {
  152. if (column.Key == pkName)
  153. continue;
  154. var colParam = cmd.CreateParameter();
  155. colParam.ParameterName = column.Key;
  156. SetParamValue( colParam, column.Value, stringParserList );
  157. cmd.Parameters.Add(colParam);
  158. sb.Append(GetParameterName(providerFactory, commandBuilder, column.Key)).Append(", ");
  159. }
  160. sb.Length = sb.Length - 2;
  161. sb.Append(")");
  162. cmd.CommandText = sb.ToString();
  163. try
  164. {
  165. cmd.ExecuteNonQuery();
  166. }
  167. catch (Exception e)
  168. {
  169. log.WarnException(
  170. "Failure to replicate changes to relational database for: " + cfg.Name + " (doc: "+ itemToReplicate.DocumentId +" ), will continue trying." +
  171. Environment.NewLine + cmd.CommandText, e);
  172. replicationStatistics.RecordWriteError(e, database);
  173. hadErrors = true;
  174. }
  175. }
  176. }
  177. }
  178. public void DeleteItems(string tableName, string pkName, bool doNotParameterize, List<string> identifiers)
  179. {
  180. const int maxParams = 1000;
  181. using (var cmd = connection.CreateCommand())
  182. {
  183. cmd.Transaction = tx;
  184. database.WorkContext.CancellationToken.ThrowIfCancellationRequested();
  185. for (int i = 0; i < identifiers.Count; i += maxParams)
  186. {
  187. cmd.Parameters.Clear();
  188. var sb = new StringBuilder("DELETE FROM ")
  189. .Append(commandBuilder.QuoteIdentifier(tableName))
  190. .Append(" WHERE ")
  191. .Append(commandBuilder.QuoteIdentifier(pkName))
  192. .Append(" IN (");
  193. for (int j = i; j < Math.Min(i + maxParams, identifiers.Count); j++)
  194. {
  195. if (i != j)
  196. sb.Append(", ");
  197. if (doNotParameterize == false)
  198. {
  199. var dbParameter = cmd.CreateParameter();
  200. dbParameter.ParameterName = GetParameterName(providerFactory, commandBuilder, "p" + j);
  201. dbParameter.Value = identifiers[j];
  202. cmd.Parameters.Add(dbParameter);
  203. sb.Append(dbParameter.ParameterName);
  204. }
  205. else
  206. {
  207. sb.Append("'").Append(SanitizeSqlValue(identifiers[j])).Append("'");
  208. }
  209. }
  210. sb.Append(")");
  211. cmd.CommandText = sb.ToString();
  212. try
  213. {
  214. cmd.ExecuteNonQuery();
  215. }
  216. catch (Exception e)
  217. {
  218. log.WarnException(
  219. "Failure to replicate changes to relational database for: " + cfg.Name + ", will continue trying." +
  220. Environment.NewLine + cmd.CommandText, e);
  221. replicationStatistics.RecordWriteError(e, database);
  222. hadErrors = true;
  223. }
  224. }
  225. }
  226. }
  227. public string SanitizeSqlValue(string sqlValue)
  228. {
  229. return sqlValue.Replace("'", "''");
  230. }
  231. private static string GetParameterName(DbProviderFactory providerFactory, DbCommandBuilder commandBuilder, string paramName)
  232. {
  233. switch (providerFactory.GetType().Name)
  234. {
  235. case "SqlClientFactory":
  236. case "MySqlClientFactory":
  237. return "@" + paramName;
  238. case "OracleClientFactory":
  239. case "NpgsqlFactory":
  240. return ":" + paramName;
  241. default:
  242. // If we don't know, try to get it from the CommandBuilder.
  243. return getParameterNameFromBuilder(commandBuilder, paramName);
  244. }
  245. }
  246. private static readonly Func<DbCommandBuilder, string, string> getParameterNameFromBuilder =
  247. (Func<DbCommandBuilder, string, string>)
  248. Delegate.CreateDelegate(typeof(Func<DbCommandBuilder, string, string>),
  249. typeof(DbCommandBuilder).GetMethod("GetParameterName",
  250. BindingFlags.Instance | BindingFlags.NonPublic, Type.DefaultBinder,
  251. new[] { typeof(string) }, null));
  252. private static void SetParamValue(DbParameter colParam, RavenJToken val, List<Func<DbParameter, String, Boolean>> stringParsers)
  253. {
  254. if (val == null)
  255. colParam.Value = DBNull.Value;
  256. else
  257. {
  258. switch (val.Type)
  259. {
  260. case JTokenType.None:
  261. case JTokenType.Object:
  262. case JTokenType.Uri:
  263. case JTokenType.Raw:
  264. case JTokenType.Array:
  265. colParam.Value = val.Value<string>();
  266. return;
  267. case JTokenType.String:
  268. var value = val.Value<string>();
  269. if( value.Length > 0 && stringParsers != null ) {
  270. foreach( var parser in stringParsers ) {
  271. if( parser( colParam, value ) ) {
  272. return;
  273. }
  274. }
  275. }
  276. colParam.Value = value;
  277. return;
  278. case JTokenType.Integer:
  279. case JTokenType.Date:
  280. case JTokenType.Bytes:
  281. case JTokenType.Guid:
  282. case JTokenType.Boolean:
  283. case JTokenType.TimeSpan:
  284. case JTokenType.Float:
  285. colParam.Value = val.Value<object>();
  286. return;
  287. case JTokenType.Null:
  288. case JTokenType.Undefined:
  289. colParam.Value = DBNull.Value;
  290. return;
  291. default:
  292. throw new InvalidOperationException("Cannot understand how to save " + val.Type + " for " + colParam.ParameterName);
  293. }
  294. }
  295. }
  296. private DbProviderFactory GetDbProviderFactory(SqlReplicationConfig cfg)
  297. {
  298. DbProviderFactory providerFactory;
  299. try
  300. {
  301. providerFactory = DbProviderFactories.GetFactory(cfg.FactoryName);
  302. }
  303. catch (Exception e)
  304. {
  305. log.WarnException(
  306. string.Format("Could not find provider factory {0} to replicate to sql for {1}, ignoring", cfg.FactoryName,
  307. cfg.Name), e);
  308. database.AddAlert(new Alert
  309. {
  310. AlertLevel = AlertLevel.Error,
  311. CreatedAt = SystemTime.UtcNow,
  312. Exception = e.ToString(),
  313. Title = "Sql Replication could not find factory provider",
  314. Message = string.Format("Could not find factory provider {0} to replicate to sql for {1}, ignoring", cfg.FactoryName,
  315. cfg.Name),
  316. UniqueKey = string.Format("Sql Replication Provider Not Found: {0}, {1}", cfg.Name, cfg.FactoryName)
  317. });
  318. throw;
  319. }
  320. return providerFactory;
  321. }
  322. public void Dispose()
  323. {
  324. tx.Dispose();
  325. commandBuilder.Dispose();
  326. connection.Dispose();
  327. }
  328. }
  329. }