SqlCommandBuilder.cs 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491
  1. //
  2. // System.Data.SqlClient.SqlCommandBuilder.cs
  3. //
  4. // Author:
  5. // Tim Coleman ([email protected])
  6. //
  7. // Copyright (C) Tim Coleman, 2002
  8. //
  9. using System;
  10. using System.Collections;
  11. using System.ComponentModel;
  12. using System.Data;
  13. using System.Data.Common;
  14. using System.Text;
  15. namespace System.Data.SqlClient {
  16. public sealed class SqlCommandBuilder : Component
  17. {
  18. #region Fields
  19. DataTable dbSchemaTable;
  20. SqlDataAdapter adapter;
  21. string quotePrefix;
  22. string quoteSuffix;
  23. string[] columnNames;
  24. string tableName;
  25. SqlCommand deleteCommand;
  26. SqlCommand insertCommand;
  27. SqlCommand updateCommand;
  28. // Used to construct WHERE clauses
  29. static readonly string clause1 = "({0} IS NULL AND {1} IS NULL)";
  30. static readonly string clause2 = "({0} = {1})";
  31. #endregion // Fields
  32. #region Constructors
  33. public SqlCommandBuilder ()
  34. {
  35. dbSchemaTable = null;
  36. adapter = null;
  37. quoteSuffix = String.Empty;
  38. quotePrefix = String.Empty;
  39. }
  40. public SqlCommandBuilder (SqlDataAdapter adapter)
  41. : this ()
  42. {
  43. DataAdapter = adapter;
  44. }
  45. #endregion // Constructors
  46. #region Properties
  47. [DataSysDescription ("The DataAdapter for which to automatically generate SqlCommands")]
  48. [DefaultValue (null)]
  49. public SqlDataAdapter DataAdapter {
  50. get { return adapter; }
  51. set {
  52. adapter = value;
  53. if (adapter != null)
  54. adapter.RowUpdating += new SqlRowUpdatingEventHandler (RowUpdatingHandler);
  55. }
  56. }
  57. private string QuotedTableName {
  58. get { return GetQuotedString (tableName); }
  59. }
  60. [Browsable (false)]
  61. [DataSysDescription ("The character used in a text command as the opening quote for quoting identifiers that contain special characters.")]
  62. [DesignerSerializationVisibility (DesignerSerializationVisibility.Hidden)]
  63. public string QuotePrefix {
  64. get { return quotePrefix; }
  65. set {
  66. if (dbSchemaTable != null)
  67. throw new InvalidOperationException ("The QuotePrefix and QuoteSuffix properties cannot be changed once an Insert, Update, or Delete command has been generated.");
  68. quotePrefix = value;
  69. }
  70. }
  71. [Browsable (false)]
  72. [DataSysDescription ("The character used in a text command as the closing quote for quoting identifiers that contain special characters.")]
  73. [DesignerSerializationVisibility (DesignerSerializationVisibility.Hidden)]
  74. public string QuoteSuffix {
  75. get { return quoteSuffix; }
  76. set {
  77. if (dbSchemaTable != null)
  78. throw new InvalidOperationException ("The QuotePrefix and QuoteSuffix properties cannot be changed once an Insert, Update, or Delete command has been generated.");
  79. quoteSuffix = value;
  80. }
  81. }
  82. private SqlCommand SourceCommand {
  83. get {
  84. if (adapter != null)
  85. return adapter.SelectCommand;
  86. return null;
  87. }
  88. }
  89. #endregion // Properties
  90. #region Methods
  91. private void BuildCache (bool closeConnection)
  92. {
  93. SqlCommand sourceCommand = SourceCommand;
  94. if (sourceCommand == null)
  95. throw new InvalidOperationException ("The DataAdapter.SelectCommand property needs to be initialized.");
  96. SqlConnection connection = sourceCommand.Connection;
  97. if (connection == null)
  98. throw new InvalidOperationException ("The DataAdapter.SelectCommand.Connection property needs to be initialized.");
  99. if (dbSchemaTable == null) {
  100. if (connection.State == ConnectionState.Open)
  101. closeConnection = false;
  102. else
  103. connection.Open ();
  104. SqlDataReader reader = sourceCommand.ExecuteReader (CommandBehavior.SchemaOnly | CommandBehavior.KeyInfo);
  105. dbSchemaTable = reader.GetSchemaTable ();
  106. reader.Close ();
  107. if (closeConnection)
  108. connection.Close ();
  109. BuildInformation (dbSchemaTable);
  110. }
  111. }
  112. private void BuildInformation (DataTable schemaTable)
  113. {
  114. tableName = String.Empty;
  115. foreach (DataRow schemaRow in schemaTable.Rows) {
  116. if (tableName == String.Empty)
  117. tableName = (string) schemaRow ["BaseTableName"];
  118. if (tableName != (string) schemaRow["BaseTableName"])
  119. throw new InvalidOperationException ("Dynamic SQL generation is not supported against multiple base tables.");
  120. }
  121. dbSchemaTable = schemaTable;
  122. }
  123. private SqlCommand CreateDeleteCommand (DataRow row, DataTableMapping tableMapping)
  124. {
  125. // If no table was found, then we can't do an delete
  126. if (QuotedTableName == String.Empty)
  127. return null;
  128. CreateNewCommand (ref deleteCommand);
  129. string command = String.Format ("DELETE FROM {0} ", QuotedTableName);
  130. StringBuilder columns = new StringBuilder ();
  131. StringBuilder where = new StringBuilder ();
  132. string dsColumnName = String.Empty;
  133. bool keyFound = false;
  134. int parmIndex = 1;
  135. foreach (DataRow schemaRow in dbSchemaTable.Rows) {
  136. if (!IncludedInWhereClause (schemaRow))
  137. continue;
  138. if (where.Length > 0)
  139. where.Append (" AND ");
  140. bool isKey = (bool) schemaRow ["IsKey"];
  141. SqlParameter parameter = null;
  142. if (!isKey) {
  143. parameter = deleteCommand.Parameters.Add (CreateParameter (parmIndex++, schemaRow));
  144. dsColumnName = tableMapping.ColumnMappings [parameter.SourceColumn].DataSetColumn;
  145. if (row != null)
  146. parameter.Value = row [dsColumnName, DataRowVersion.Current];
  147. where.Append ("(");
  148. where.Append (String.Format (clause1, GetQuotedString (parameter.SourceColumn), parameter.ParameterName));
  149. where.Append (" OR ");
  150. }
  151. else
  152. keyFound = true;
  153. parameter = deleteCommand.Parameters.Add (CreateParameter (parmIndex++, schemaRow));
  154. dsColumnName = tableMapping.ColumnMappings [parameter.SourceColumn].DataSetColumn;
  155. if (row != null)
  156. parameter.Value = row [dsColumnName, DataRowVersion.Current];
  157. where.Append (String.Format (clause2, GetQuotedString (parameter.SourceColumn), parameter.ParameterName));
  158. if (!isKey)
  159. where.Append (")");
  160. }
  161. if (!keyFound)
  162. throw new InvalidOperationException ("Dynamic SQL generation for the DeleteCommand is not supported against a SelectCommand that does not return any key column information.");
  163. // We're all done, so bring it on home
  164. string sql = String.Format ("{0} WHERE ( {1} )", command, where.ToString ());
  165. deleteCommand.CommandText = sql;
  166. return deleteCommand;
  167. }
  168. private SqlCommand CreateInsertCommand (DataRow row, DataTableMapping tableMapping)
  169. {
  170. if (QuotedTableName == String.Empty)
  171. return null;
  172. CreateNewCommand (ref insertCommand);
  173. string command = String.Format ("INSERT INTO {0}", QuotedTableName);
  174. string sql;
  175. StringBuilder columns = new StringBuilder ();
  176. StringBuilder values = new StringBuilder ();
  177. string dsColumnName = String.Empty;
  178. int parmIndex = 1;
  179. foreach (DataRow schemaRow in dbSchemaTable.Rows) {
  180. if (!IncludedInInsert (schemaRow))
  181. continue;
  182. if (parmIndex > 1) {
  183. columns.Append (" , ");
  184. values.Append (" , ");
  185. }
  186. SqlParameter parameter = insertCommand.Parameters.Add (CreateParameter (parmIndex++, schemaRow));
  187. dsColumnName = tableMapping.ColumnMappings [parameter.SourceColumn].DataSetColumn;
  188. if (row != null)
  189. parameter.Value = row [dsColumnName];
  190. columns.Append (GetQuotedString (parameter.SourceColumn));
  191. values.Append (parameter.ParameterName);
  192. }
  193. sql = String.Format ("{0}( {1} ) VALUES ( {2} )", command, columns.ToString (), values.ToString ());
  194. insertCommand.CommandText = sql;
  195. return insertCommand;
  196. }
  197. private void CreateNewCommand (ref SqlCommand command)
  198. {
  199. SqlCommand sourceCommand = SourceCommand;
  200. if (command == null) {
  201. command = sourceCommand.Connection.CreateCommand ();
  202. command.CommandTimeout = sourceCommand.CommandTimeout;
  203. command.Transaction = sourceCommand.Transaction;
  204. }
  205. command.CommandType = CommandType.Text;
  206. command.UpdatedRowSource = UpdateRowSource.None;
  207. }
  208. private SqlCommand CreateUpdateCommand (DataRow row, DataTableMapping tableMapping)
  209. {
  210. // If no table was found, then we can't do an update
  211. if (QuotedTableName == String.Empty)
  212. return null;
  213. CreateNewCommand (ref updateCommand);
  214. string command = String.Format ("UPDATE {0} SET ", QuotedTableName);
  215. StringBuilder columns = new StringBuilder ();
  216. StringBuilder where = new StringBuilder ();
  217. int parmIndex = 1;
  218. string dsColumnName = String.Empty;
  219. bool keyFound = false;
  220. // First, create the X=Y list for UPDATE
  221. foreach (DataRow schemaRow in dbSchemaTable.Rows) {
  222. if (columns.Length > 0)
  223. columns.Append (" , ");
  224. SqlParameter parameter = updateCommand.Parameters.Add (CreateParameter (parmIndex++, schemaRow));
  225. dsColumnName = tableMapping.ColumnMappings [parameter.SourceColumn].DataSetColumn;
  226. if (row != null)
  227. parameter.Value = row [dsColumnName, DataRowVersion.Proposed];
  228. columns.Append (String.Format ("{0} = {1}", GetQuotedString (parameter.SourceColumn), parameter.ParameterName));
  229. }
  230. // Now, create the WHERE clause. This may be optimizable, but it would be ugly to incorporate
  231. // into the loop above. "Premature optimization is the root of all evil." -- Knuth
  232. foreach (DataRow schemaRow in dbSchemaTable.Rows) {
  233. if (!IncludedInWhereClause (schemaRow))
  234. continue;
  235. if (where.Length > 0)
  236. where.Append (" AND ");
  237. bool isKey = (bool) schemaRow ["IsKey"];
  238. SqlParameter parameter = null;
  239. if (!isKey) {
  240. parameter = updateCommand.Parameters.Add (CreateParameter (parmIndex++, schemaRow));
  241. dsColumnName = tableMapping.ColumnMappings [parameter.SourceColumn].DataSetColumn;
  242. if (row != null)
  243. parameter.Value = row [dsColumnName];
  244. where.Append ("(");
  245. where.Append (String.Format (clause1, GetQuotedString (parameter.SourceColumn), parameter.ParameterName));
  246. where.Append (" OR ");
  247. }
  248. else
  249. keyFound = true;
  250. parameter = updateCommand.Parameters.Add (CreateParameter (parmIndex++, schemaRow));
  251. dsColumnName = tableMapping.ColumnMappings [parameter.SourceColumn].DataSetColumn;
  252. if (row != null)
  253. parameter.Value = row [dsColumnName];
  254. where.Append (String.Format (clause2, GetQuotedString (parameter.SourceColumn), parameter.ParameterName));
  255. if (!isKey)
  256. where.Append (")");
  257. }
  258. if (!keyFound)
  259. throw new InvalidOperationException ("Dynamic SQL generation for the UpdateCommand is not supported against a SelectCommand that does not return any key column information.");
  260. // We're all done, so bring it on home
  261. string sql = String.Format ("{0}{1} WHERE ( {2} )", command, columns.ToString (), where.ToString ());
  262. updateCommand.CommandText = sql;
  263. return updateCommand;
  264. }
  265. private SqlParameter CreateParameter (int parmIndex, DataRow schemaRow)
  266. {
  267. string name = String.Format ("@p{0}", parmIndex);
  268. string sourceColumn = (string) schemaRow ["BaseColumnName"];
  269. SqlDbType sqlDbType = (SqlDbType) schemaRow ["ProviderType"];
  270. int size = (int) schemaRow ["ColumnSize"];
  271. return new SqlParameter (name, sqlDbType, size, sourceColumn);
  272. }
  273. public static void DeriveParameters (SqlCommand command)
  274. {
  275. command.DeriveParameters ();
  276. }
  277. [MonoTODO ("Determine what should be disposed.")]
  278. protected override void Dispose (bool disposing)
  279. {
  280. // Should dispose the commands, schema table, and should remove itself from the
  281. // message queues
  282. if (disposing) {
  283. if (insertCommand != null)
  284. insertCommand.Dispose ();
  285. if (deleteCommand != null)
  286. deleteCommand.Dispose ();
  287. if (updateCommand != null)
  288. updateCommand.Dispose ();
  289. if (dbSchemaTable != null)
  290. dbSchemaTable.Dispose ();
  291. }
  292. }
  293. public SqlCommand GetDeleteCommand ()
  294. {
  295. BuildCache (true);
  296. return CreateDeleteCommand (null, null);
  297. }
  298. public SqlCommand GetInsertCommand ()
  299. {
  300. BuildCache (true);
  301. return CreateInsertCommand (null, null);
  302. }
  303. private string GetQuotedString (string value)
  304. {
  305. if (value == String.Empty || value == null)
  306. return value;
  307. if (quotePrefix == String.Empty && quoteSuffix == String.Empty)
  308. return value;
  309. return String.Format ("{0}{1}{2}", quotePrefix, value, quoteSuffix);
  310. }
  311. public SqlCommand GetUpdateCommand ()
  312. {
  313. BuildCache (true);
  314. return CreateUpdateCommand (null, null);
  315. }
  316. private bool IncludedInInsert (DataRow schemaRow)
  317. {
  318. // If the parameter has one of these properties, then we don't include it in the insert:
  319. // AutoIncrement, Hidden, Expression, RowVersion, ReadOnly
  320. if ((bool) schemaRow ["IsAutoIncrement"])
  321. return false;
  322. if ((bool) schemaRow ["IsHidden"])
  323. return false;
  324. if ((bool) schemaRow ["IsExpression"])
  325. return false;
  326. if ((bool) schemaRow ["IsRowVersion"])
  327. return false;
  328. if ((bool) schemaRow ["IsReadOnly"])
  329. return false;
  330. return true;
  331. }
  332. private bool IncludedInUpdate (DataRow schemaRow)
  333. {
  334. // If the parameter has one of these properties, then we don't include it in the insert:
  335. // AutoIncrement, Hidden, RowVersion
  336. if ((bool) schemaRow ["IsAutoIncrement"])
  337. return false;
  338. if ((bool) schemaRow ["IsHidden"])
  339. return false;
  340. if ((bool) schemaRow ["IsRowVersion"])
  341. return false;
  342. return true;
  343. }
  344. private bool IncludedInWhereClause (DataRow schemaRow)
  345. {
  346. if ((bool) schemaRow ["IsLong"])
  347. return false;
  348. return true;
  349. }
  350. [MonoTODO ("Figure out what else needs to be cleaned up when we refresh.")]
  351. public void RefreshSchema ()
  352. {
  353. tableName = String.Empty;
  354. dbSchemaTable = null;
  355. }
  356. #endregion // Methods
  357. #region Event Handlers
  358. private void RowUpdatingHandler (object sender, SqlRowUpdatingEventArgs e)
  359. {
  360. if (e.Status != UpdateStatus.Continue)
  361. return;
  362. switch (e.StatementType) {
  363. case StatementType.Delete:
  364. deleteCommand = e.Command;
  365. break;
  366. case StatementType.Insert:
  367. insertCommand = e.Command;
  368. break;
  369. case StatementType.Update:
  370. updateCommand = e.Command;
  371. break;
  372. default:
  373. return;
  374. }
  375. try {
  376. BuildCache (false);
  377. switch (e.StatementType) {
  378. case StatementType.Delete:
  379. e.Command = CreateDeleteCommand (e.Row, e.TableMapping);
  380. e.Status = UpdateStatus.Continue;
  381. break;
  382. case StatementType.Insert:
  383. e.Command = CreateInsertCommand (e.Row, e.TableMapping);
  384. e.Status = UpdateStatus.Continue;
  385. break;
  386. case StatementType.Update:
  387. e.Command = CreateUpdateCommand (e.Row, e.TableMapping);
  388. e.Status = UpdateStatus.Continue;
  389. break;
  390. }
  391. if (e.Command != null && e.Row != null) {
  392. e.Row.AcceptChanges ();
  393. e.Status = UpdateStatus.SkipCurrentRow;
  394. }
  395. }
  396. catch (Exception exception) {
  397. e.Errors = exception;
  398. e.Status = UpdateStatus.ErrorsOccurred;
  399. }
  400. }
  401. #endregion // Event Handlers
  402. }
  403. }