ForeignKeyConstraint.cs 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560
  1. //
  2. // System.Data.ForeignKeyConstraint.cs
  3. //
  4. // Author:
  5. // Franklin Wise <[email protected]>
  6. // Daniel Morgan <[email protected]>
  7. //
  8. // (C) 2002 Franklin Wise
  9. // (C) 2002 Daniel Morgan
  10. //
  11. //
  12. // Copyright (C) 2004 Novell, Inc (http://www.novell.com)
  13. //
  14. // Permission is hereby granted, free of charge, to any person obtaining
  15. // a copy of this software and associated documentation files (the
  16. // "Software"), to deal in the Software without restriction, including
  17. // without limitation the rights to use, copy, modify, merge, publish,
  18. // distribute, sublicense, and/or sell copies of the Software, and to
  19. // permit persons to whom the Software is furnished to do so, subject to
  20. // the following conditions:
  21. //
  22. // The above copyright notice and this permission notice shall be
  23. // included in all copies or substantial portions of the Software.
  24. //
  25. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
  26. // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
  27. // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
  28. // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
  29. // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
  30. // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
  31. // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
  32. //
  33. using System;
  34. using System.Collections;
  35. using System.ComponentModel;
  36. using System.Runtime.InteropServices;
  37. using System.Data.Common;
  38. namespace System.Data {
  39. [Editor]
  40. [DefaultProperty ("ConstraintName")]
  41. [Serializable]
  42. public class ForeignKeyConstraint : Constraint
  43. {
  44. private UniqueConstraint _parentUniqueConstraint;
  45. //FIXME: create a class which will wrap this collection
  46. private DataColumn [] _parentColumns;
  47. //FIXME: create a class which will wrap this collection
  48. private DataColumn [] _childColumns;
  49. private Rule _deleteRule = Rule.Cascade;
  50. private Rule _updateRule = Rule.Cascade;
  51. private AcceptRejectRule _acceptRejectRule = AcceptRejectRule.None;
  52. private string _parentTableName;
  53. private string _childTableName;
  54. //FIXME: remove those; and use only DataColumns[]
  55. private string [] _parentColumnNames;
  56. private string [] _childColumnNames;
  57. private bool _dataColsNotValidated = false;
  58. #region Constructors
  59. public ForeignKeyConstraint(DataColumn parentColumn, DataColumn childColumn)
  60. {
  61. if (null == parentColumn || null == childColumn) {
  62. throw new NullReferenceException("Neither parentColumn or" +
  63. " childColumn can be null.");
  64. }
  65. _foreignKeyConstraint(null, new DataColumn[] {parentColumn},
  66. new DataColumn[] {childColumn});
  67. }
  68. public ForeignKeyConstraint(DataColumn[] parentColumns, DataColumn[] childColumns)
  69. {
  70. _foreignKeyConstraint(null, parentColumns, childColumns);
  71. }
  72. public ForeignKeyConstraint(string constraintName, DataColumn parentColumn, DataColumn childColumn)
  73. {
  74. if (null == parentColumn || null == childColumn) {
  75. throw new NullReferenceException("Neither parentColumn or" +
  76. " childColumn can be null.");
  77. }
  78. _foreignKeyConstraint(constraintName, new DataColumn[] {parentColumn},
  79. new DataColumn[] {childColumn});
  80. }
  81. public ForeignKeyConstraint(string constraintName, DataColumn[] parentColumns, DataColumn[] childColumns)
  82. {
  83. _foreignKeyConstraint(constraintName, parentColumns, childColumns);
  84. }
  85. //special case
  86. [Browsable (false)]
  87. public ForeignKeyConstraint(string constraintName, string parentTableName, string[] parentColumnNames, string[] childColumnNames, AcceptRejectRule acceptRejectRule, Rule deleteRule, Rule updateRule)
  88. {
  89. _dataColsNotValidated = true;
  90. base.ConstraintName = constraintName;
  91. // "parentTableName" is searched in the "DataSet" to which the "DataTable"
  92. // from which AddRange() is called
  93. // childTable is the "DataTable" which calls AddRange()
  94. // Keep reference to parentTableName to resolve later
  95. _parentTableName = parentTableName;
  96. // Keep reference to parentColumnNames to resolve later
  97. _parentColumnNames = parentColumnNames;
  98. // Keep reference to childColumnNames to resolve later
  99. _childColumnNames = childColumnNames;
  100. _acceptRejectRule = acceptRejectRule;
  101. _deleteRule = deleteRule;
  102. _updateRule = updateRule;
  103. }
  104. internal void postAddRange (DataTable childTable)
  105. {
  106. // LAMESPEC - Does not say that this is mandatory
  107. // Check whether childTable belongs to a DataSet
  108. if (childTable.DataSet == null)
  109. throw new InvalidConstraintException ("ChildTable : " + childTable.TableName + " does not belong to any DataSet");
  110. DataSet dataSet = childTable.DataSet;
  111. _childTableName = childTable.TableName;
  112. // Search for the parentTable in the childTable's DataSet
  113. if (!dataSet.Tables.Contains (_parentTableName))
  114. throw new InvalidConstraintException ("Table : " + _parentTableName + "does not exist in DataSet : " + dataSet);
  115. // Keep reference to parentTable
  116. DataTable parentTable = dataSet.Tables [_parentTableName];
  117. int i = 0, j = 0;
  118. // LAMESPEC - Does not say which Exception is thrown
  119. if (_parentColumnNames.Length < 0 || _childColumnNames.Length < 0)
  120. throw new InvalidConstraintException ("Neither parent nor child columns can be zero length");
  121. // LAMESPEC - Does not say which Exception is thrown
  122. if (_parentColumnNames.Length != _childColumnNames.Length)
  123. throw new InvalidConstraintException ("Both parent and child columns must be of same length");
  124. DataColumn []parentColumns = new DataColumn [_parentColumnNames.Length];
  125. DataColumn []childColumns = new DataColumn [_childColumnNames.Length];
  126. // Search for the parentColumns in parentTable
  127. foreach (string parentCol in _parentColumnNames){
  128. if (!parentTable.Columns.Contains (parentCol))
  129. throw new InvalidConstraintException ("Table : " + _parentTableName + "does not contain the column :" + parentCol);
  130. parentColumns [i++] = parentTable. Columns [parentCol];
  131. }
  132. // Search for the childColumns in childTable
  133. foreach (string childCol in _childColumnNames){
  134. if (!childTable.Columns.Contains (childCol))
  135. throw new InvalidConstraintException ("Table : " + _childTableName + "does not contain the column : " + childCol);
  136. childColumns [j++] = childTable.Columns [childCol];
  137. }
  138. _validateColumns (parentColumns, childColumns);
  139. _parentColumns = parentColumns;
  140. _childColumns = childColumns;
  141. }
  142. #if NET_2_0
  143. [MonoTODO]
  144. public ForeignKeyConstraint (string constraintName, string parentTableName, string parentTableNamespace, string[] parentColumnNames, string[] childColumnNames, AcceptRejectRule acceptRejectRule, Rule deleteRule, Rule updateRule)
  145. {
  146. throw new NotImplementedException ();
  147. }
  148. #endif
  149. private void _foreignKeyConstraint(string constraintName, DataColumn[] parentColumns,
  150. DataColumn[] childColumns)
  151. {
  152. //Validate
  153. _validateColumns(parentColumns, childColumns);
  154. //Set Constraint Name
  155. base.ConstraintName = constraintName;
  156. //Keep reference to columns
  157. _parentColumns = parentColumns;
  158. _childColumns = childColumns;
  159. }
  160. #endregion // Constructors
  161. #region Helpers
  162. private void _validateColumns(DataColumn[] parentColumns, DataColumn[] childColumns)
  163. {
  164. //not null
  165. if (null == parentColumns || null == childColumns)
  166. throw new ArgumentNullException();
  167. //at least one element in each array
  168. if (parentColumns.Length < 1 || childColumns.Length < 1)
  169. throw new ArgumentException("Neither ParentColumns or ChildColumns can't be" +
  170. " zero length.");
  171. //same size arrays
  172. if (parentColumns.Length != childColumns.Length)
  173. throw new ArgumentException("Parent columns and child columns must be the same length.");
  174. DataTable ptable = parentColumns[0].Table;
  175. DataTable ctable = childColumns[0].Table;
  176. for (int i = 0; i < parentColumns.Length; i++) {
  177. DataColumn pc = parentColumns[i];
  178. DataColumn cc = childColumns[i];
  179. //not null check
  180. if (null == pc.Table)
  181. throw new ArgumentException("All columns must belong to a table." +
  182. " ColumnName: " + pc.ColumnName + " does not belong to a table.");
  183. //All columns must belong to the same table
  184. if (ptable != pc.Table)
  185. throw new InvalidConstraintException("Parent columns must all belong to the same table.");
  186. //not null check
  187. if (null == cc.Table)
  188. throw new ArgumentException("All columns must belong to a table." +
  189. " ColumnName: " + pc.ColumnName + " does not belong to a table.");
  190. //All columns must belong to the same table.
  191. if (ctable != cc.Table)
  192. throw new InvalidConstraintException("Child columns must all belong to the same table.");
  193. if (pc.CompiledExpression != null)
  194. throw new ArgumentException(String.Format("Cannot create a constraint based on Expression column {0}.", pc.ColumnName));
  195. if (cc.CompiledExpression != null)
  196. throw new ArgumentException(String.Format("Cannot create a constraint based on Expression column {0}.", cc.ColumnName));
  197. }
  198. //Same dataset. If both are null it's ok
  199. if (ptable.DataSet != ctable.DataSet)
  200. {
  201. //LAMESPEC: spec says InvalidConstraintExceptoin
  202. // impl does InvalidOperationException
  203. throw new InvalidOperationException("Parent column and child column must belong to" +
  204. " tables that belong to the same DataSet.");
  205. }
  206. for (int i = 0; i < parentColumns.Length; i++)
  207. {
  208. DataColumn pc = parentColumns[i];
  209. DataColumn cc = childColumns[i];
  210. //Can't be the same column
  211. if (pc == cc)
  212. throw new InvalidOperationException("Parent and child columns can't be the same column.");
  213. if (! pc.DataType.Equals(cc.DataType))
  214. {
  215. //LAMESPEC: spec says throw InvalidConstraintException
  216. // implementation throws InvalidOperationException
  217. throw new InvalidConstraintException("Parent column is not type compatible with it's child"
  218. + " column.");
  219. }
  220. }
  221. }
  222. private void _validateRemoveParentConstraint(ConstraintCollection sender,
  223. Constraint constraint, ref bool cancel, ref string failReason)
  224. {
  225. #if !NET_1_1
  226. //if we hold a reference to the parent then cancel it
  227. if (constraint == _parentUniqueConstraint)
  228. {
  229. cancel = true;
  230. failReason = "Cannot remove UniqueConstraint because the"
  231. + " ForeignKeyConstraint " + this.ConstraintName + " exists.";
  232. }
  233. #endif
  234. }
  235. //Checks to see if a related unique constraint exists
  236. //if it doesn't then a unique constraint is created.
  237. //if a unique constraint can't be created an exception will be thrown
  238. private void _ensureUniqueConstraintExists(ConstraintCollection collection,
  239. DataColumn [] parentColumns)
  240. {
  241. //not null
  242. if (null == parentColumns) throw new ArgumentNullException(
  243. "ParentColumns can't be null");
  244. UniqueConstraint uc = null;
  245. //see if unique constraint already exists
  246. //if not create unique constraint
  247. if(parentColumns[0] != null)
  248. uc = UniqueConstraint.GetUniqueConstraintForColumnSet(parentColumns[0].Table.Constraints, parentColumns);
  249. if (null == uc) {
  250. uc = new UniqueConstraint(parentColumns, false); //could throw
  251. parentColumns [0].Table.Constraints.Add (uc);
  252. }
  253. //keep reference
  254. _parentUniqueConstraint = uc;
  255. //parentColumns [0].Table.Constraints.Add (uc);
  256. //if this unique constraint is attempted to be removed before us
  257. //we can fail the validation
  258. //collection.ValidateRemoveConstraint += new DelegateValidateRemoveConstraint(
  259. // _validateRemoveParentConstraint);
  260. }
  261. #endregion //Helpers
  262. #region Properties
  263. [DataCategory ("Data")]
  264. [DataSysDescription ("For accept and reject changes, indicates what kind of cascading should take place across this relation.")]
  265. [DefaultValue (AcceptRejectRule.None)]
  266. public virtual AcceptRejectRule AcceptRejectRule {
  267. get { return _acceptRejectRule; }
  268. set { _acceptRejectRule = value; }
  269. }
  270. [DataCategory ("Data")]
  271. [DataSysDescription ("Indicates the child columns of this constraint.")]
  272. [ReadOnly (true)]
  273. public virtual DataColumn[] Columns {
  274. get { return _childColumns; }
  275. }
  276. [DataCategory ("Data")]
  277. [DataSysDescription ("For deletions, indicates what kind of cascading should take place across this relation.")]
  278. [DefaultValue (Rule.Cascade)]
  279. public virtual Rule DeleteRule {
  280. get { return _deleteRule; }
  281. set { _deleteRule = value; }
  282. }
  283. [DataCategory ("Data")]
  284. [DataSysDescription ("For updates, indicates what kind of cascading should take place across this relation.")]
  285. [DefaultValue (Rule.Cascade)]
  286. public virtual Rule UpdateRule {
  287. get { return _updateRule; }
  288. set { _updateRule = value; }
  289. }
  290. [DataCategory ("Data")]
  291. [DataSysDescription ("Indicates the parent columns of this constraint.")]
  292. [ReadOnly (true)]
  293. public virtual DataColumn[] RelatedColumns {
  294. get { return _parentColumns; }
  295. }
  296. [DataCategory ("Data")]
  297. [DataSysDescription ("Indicates the child table of this constraint.")]
  298. [ReadOnly (true)]
  299. public virtual DataTable RelatedTable {
  300. get {
  301. if (_parentColumns != null)
  302. if (_parentColumns.Length > 0)
  303. return _parentColumns[0].Table;
  304. throw new InvalidOperationException ("Property not accessible because 'Object reference not set to an instance of an object'");
  305. }
  306. }
  307. [DataCategory ("Data")]
  308. [DataSysDescription ("Indicates the table of this constraint.")]
  309. [ReadOnly (true)]
  310. public override DataTable Table {
  311. get {
  312. if (_childColumns != null)
  313. if (_childColumns.Length > 0)
  314. return _childColumns[0].Table;
  315. throw new InvalidOperationException ("Property not accessible because 'Object reference not set to an instance of an object'");
  316. }
  317. }
  318. internal bool DataColsNotValidated
  319. {
  320. get {
  321. return (_dataColsNotValidated);
  322. }
  323. }
  324. #endregion // Properties
  325. #region Methods
  326. public override bool Equals(object key)
  327. {
  328. ForeignKeyConstraint fkc = key as ForeignKeyConstraint;
  329. if (null == fkc) return false;
  330. //if the fk constrains the same columns then they are equal
  331. if (! DataColumn.AreColumnSetsTheSame( this.RelatedColumns, fkc.RelatedColumns))
  332. return false;
  333. if (! DataColumn.AreColumnSetsTheSame( this.Columns, fkc.Columns) )
  334. return false;
  335. return true;
  336. }
  337. public override int GetHashCode()
  338. {
  339. //initialize hash1 and hash2 with default hashes
  340. //any two DIFFERENT numbers will do here
  341. int hash1 = 32, hash2 = 88;
  342. int i;
  343. //derive the hash code from the columns that way
  344. //Equals and GetHashCode return Equal objects to be the
  345. //same
  346. //Get the first parent column hash
  347. if (this.Columns.Length > 0)
  348. hash1 ^= this.Columns[0].GetHashCode();
  349. //get the rest of the parent column hashes if there any
  350. for (i = 1; i < this.Columns.Length; i++)
  351. {
  352. hash1 ^= this.Columns[1].GetHashCode();
  353. }
  354. //Get the child column hash
  355. if (this.RelatedColumns.Length > 0)
  356. hash2 ^= this.Columns[0].GetHashCode();
  357. for (i = 1; i < this.RelatedColumns.Length; i++)
  358. {
  359. hash2 ^= this.RelatedColumns[1].GetHashCode();
  360. }
  361. //combine the two hashes
  362. return hash1 ^ hash2;
  363. }
  364. internal override void AddToConstraintCollectionSetup(
  365. ConstraintCollection collection)
  366. {
  367. if (collection.Table != Table)
  368. throw new InvalidConstraintException("This constraint cannot be added since ForeignKey doesn't belong to table " + RelatedTable.TableName + ".");
  369. //run Ctor rules again
  370. _validateColumns(_parentColumns, _childColumns);
  371. //we must have a unique constraint on the parent
  372. _ensureUniqueConstraintExists(collection, _parentColumns);
  373. //Make sure we can create this thing
  374. //AssertConstraint();
  375. if ( (Table.DataSet != null && Table.DataSet.EnforceConstraints)
  376. || (Table.DataSet == null && Table.EnforceConstraints)) {
  377. if (IsConstraintViolated())
  378. throw new ArgumentException("This constraint cannot be enabled as not all values have corresponding parent values.");
  379. }
  380. //FIXME : if this fails and we created a unique constraint
  381. //we should probably roll it back
  382. // and remove index form Table
  383. }
  384. internal override void RemoveFromConstraintCollectionCleanup(
  385. ConstraintCollection collection)
  386. {
  387. Index = null;
  388. }
  389. protected internal override bool IsConstraintViolated()
  390. {
  391. if (Table.DataSet == null || RelatedTable.DataSet == null)
  392. return false;
  393. bool hasErrors = false;
  394. foreach (DataRow row in Table.Rows) {
  395. // first we check if all values in _childColumns place are nulls.
  396. // if yes we return.
  397. if (row.IsNullColumns(_childColumns))
  398. continue;
  399. // check whenever there is (at least one) parent row in RelatedTable
  400. if(!RelatedTable.RowsExist(_parentColumns,_childColumns,row)) {
  401. // if no parent row exists - constraint is violated
  402. hasErrors = true;
  403. string[] values = new string[_childColumns.Length];
  404. for (int i = 0; i < _childColumns.Length; i++){
  405. DataColumn col = _childColumns[i];
  406. values[i] = row[col].ToString();
  407. }
  408. row.RowError = String.Format("ForeignKeyConstraint {0} requires the child key values ({1}) to exist in the parent table.",
  409. ConstraintName, String.Join(",", values));
  410. }
  411. }
  412. if (hasErrors)
  413. //throw new ConstraintException("Failed to enable constraints. One or more rows contain values violating non-null, unique, or foreign-key constraints.");
  414. return true;
  415. return false;
  416. }
  417. internal override void AssertConstraint(DataRow row)
  418. {
  419. // first we check if all values in _childColumns place are nulls.
  420. // if yes we return.
  421. if (row.IsNullColumns(_childColumns))
  422. return;
  423. // check whenever there is (at least one) parent row in RelatedTable
  424. if(!RelatedTable.RowsExist(_parentColumns,_childColumns,row)) {
  425. // if no parent row exists - constraint is violated
  426. throw new InvalidConstraintException(GetErrorMessage(row));
  427. }
  428. }
  429. internal override bool IsColumnContained(DataColumn column)
  430. {
  431. for (int i = 0; i < _parentColumns.Length; i++)
  432. if (column == _parentColumns[i])
  433. return true;
  434. for (int i = 0; i < _childColumns.Length; i++)
  435. if (column == _childColumns[i])
  436. return true;
  437. return false;
  438. }
  439. internal override bool CanRemoveFromCollection(ConstraintCollection col, bool shouldThrow){
  440. return true;
  441. }
  442. private string GetErrorMessage(DataRow row)
  443. {
  444. System.Text.StringBuilder sb = new System.Text.StringBuilder();
  445. for (int i = 0; i < _childColumns.Length; i++) {
  446. sb.Append(row[_childColumns[0]].ToString());
  447. if (i != _childColumns.Length - 1) {
  448. sb.Append(',');
  449. }
  450. }
  451. string valStr = sb.ToString();
  452. return "ForeignKeyConstraint " + ConstraintName + " requires the child key values (" + valStr + ") to exist in the parent table.";
  453. }
  454. #endregion // Methods
  455. }
  456. }