You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

FKEnforcer.java 9.6KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322
  1. /*
  2. Copyright (c) 2012 James Ahlborn
  3. This library is free software; you can redistribute it and/or
  4. modify it under the terms of the GNU Lesser General Public
  5. License as published by the Free Software Foundation; either
  6. version 2.1 of the License, or (at your option) any later version.
  7. This library is distributed in the hope that it will be useful,
  8. but WITHOUT ANY WARRANTY; without even the implied warranty of
  9. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  10. Lesser General Public License for more details.
  11. You should have received a copy of the GNU Lesser General Public
  12. License along with this library; if not, write to the Free Software
  13. Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
  14. USA
  15. */
  16. package com.healthmarketscience.jackcess.impl;
  17. import java.io.IOException;
  18. import java.util.ArrayList;
  19. import java.util.Arrays;
  20. import java.util.Collections;
  21. import java.util.Iterator;
  22. import java.util.List;
  23. import java.util.Set;
  24. import java.util.TreeSet;
  25. import com.healthmarketscience.jackcess.Column;
  26. import com.healthmarketscience.jackcess.Index;
  27. import com.healthmarketscience.jackcess.IndexCursor;
  28. import com.healthmarketscience.jackcess.Row;
  29. import com.healthmarketscience.jackcess.Table;
  30. import com.healthmarketscience.jackcess.util.CaseInsensitiveColumnMatcher;
  31. import com.healthmarketscience.jackcess.util.ColumnMatcher;
  32. import com.healthmarketscience.jackcess.util.Joiner;
  33. /**
  34. * Utility class used by Table to enforce foreign-key relationships (if
  35. * enabled).
  36. *
  37. * @author James Ahlborn
  38. * @usage _advanced_class_
  39. */
  40. final class FKEnforcer
  41. {
  42. // fk constraints always work with indexes, which are always
  43. // case-insensitive
  44. private static final ColumnMatcher MATCHER =
  45. CaseInsensitiveColumnMatcher.INSTANCE;
  46. private final TableImpl _table;
  47. private final List<ColumnImpl> _cols;
  48. private List<Joiner> _primaryJoinersChkUp;
  49. private List<Joiner> _primaryJoinersChkDel;
  50. private List<Joiner> _primaryJoinersDoUp;
  51. private List<Joiner> _primaryJoinersDoDel;
  52. private List<Joiner> _secondaryJoiners;
  53. FKEnforcer(TableImpl table) {
  54. _table = table;
  55. // at this point, only init the index columns
  56. Set<ColumnImpl> cols = new TreeSet<ColumnImpl>();
  57. for(IndexImpl idx : _table.getIndexes()) {
  58. IndexImpl.ForeignKeyReference ref = idx.getReference();
  59. if(ref != null) {
  60. // compile an ordered list of all columns in this table which are
  61. // involved in foreign key relationships with other tables
  62. for(IndexData.ColumnDescriptor iCol : idx.getColumns()) {
  63. cols.add(iCol.getColumn());
  64. }
  65. }
  66. }
  67. _cols = !cols.isEmpty() ?
  68. Collections.unmodifiableList(new ArrayList<ColumnImpl>(cols)) :
  69. Collections.<ColumnImpl>emptyList();
  70. }
  71. /**
  72. * Does secondary initialization, if necessary.
  73. */
  74. private void initialize() throws IOException {
  75. if(_secondaryJoiners != null) {
  76. // already initialized
  77. return;
  78. }
  79. // initialize all the joiners
  80. _primaryJoinersChkUp = new ArrayList<Joiner>(1);
  81. _primaryJoinersChkDel = new ArrayList<Joiner>(1);
  82. _primaryJoinersDoUp = new ArrayList<Joiner>(1);
  83. _primaryJoinersDoDel = new ArrayList<Joiner>(1);
  84. _secondaryJoiners = new ArrayList<Joiner>(1);
  85. for(IndexImpl idx : _table.getIndexes()) {
  86. IndexImpl.ForeignKeyReference ref = idx.getReference();
  87. if(ref != null) {
  88. Joiner joiner = Joiner.create(idx);
  89. if(ref.isPrimaryTable()) {
  90. if(ref.isCascadeUpdates()) {
  91. _primaryJoinersDoUp.add(joiner);
  92. } else {
  93. _primaryJoinersChkUp.add(joiner);
  94. }
  95. if(ref.isCascadeDeletes()) {
  96. _primaryJoinersDoDel.add(joiner);
  97. } else {
  98. _primaryJoinersChkDel.add(joiner);
  99. }
  100. } else {
  101. _secondaryJoiners.add(joiner);
  102. }
  103. }
  104. }
  105. }
  106. /**
  107. * Handles foregn-key constraints when adding a row.
  108. *
  109. * @param row new row in the Table's row format, including all values used
  110. * in any foreign-key relationships
  111. */
  112. public void addRow(Object[] row) throws IOException {
  113. if(!enforcing()) {
  114. return;
  115. }
  116. initialize();
  117. for(Joiner joiner : _secondaryJoiners) {
  118. requirePrimaryValues(joiner, row);
  119. }
  120. }
  121. /**
  122. * Handles foregn-key constraints when updating a row.
  123. *
  124. * @param oldRow old row in the Table's row format, including all values
  125. * used in any foreign-key relationships
  126. * @param newRow new row in the Table's row format, including all values
  127. * used in any foreign-key relationships
  128. */
  129. public void updateRow(Object[] oldRow, Object[] newRow) throws IOException {
  130. if(!enforcing()) {
  131. return;
  132. }
  133. if(!anyUpdates(oldRow, newRow)) {
  134. // no changes were made to any relevant columns
  135. return;
  136. }
  137. initialize();
  138. SharedState ss = _table.getDatabase().getFKEnforcerSharedState();
  139. if(ss.isUpdating()) {
  140. // we only check the primary relationships for the "top-level" of an
  141. // update operation. in nested levels we are only ever changing the fk
  142. // values themselves, so we always know the new values are valid.
  143. for(Joiner joiner : _secondaryJoiners) {
  144. if(anyUpdates(joiner, oldRow, newRow)) {
  145. requirePrimaryValues(joiner, newRow);
  146. }
  147. }
  148. }
  149. ss.pushUpdate();
  150. try {
  151. // now, check the tables for which we are the primary table in the
  152. // relationship (but not cascading)
  153. for(Joiner joiner : _primaryJoinersChkUp) {
  154. if(anyUpdates(joiner, oldRow, newRow)) {
  155. requireNoSecondaryValues(joiner, oldRow);
  156. }
  157. }
  158. // lastly, update the tables for which we are the primary table in the
  159. // relationship
  160. for(Joiner joiner : _primaryJoinersDoUp) {
  161. if(anyUpdates(joiner, oldRow, newRow)) {
  162. updateSecondaryValues(joiner, oldRow, newRow);
  163. }
  164. }
  165. } finally {
  166. ss.popUpdate();
  167. }
  168. }
  169. /**
  170. * Handles foregn-key constraints when deleting a row.
  171. *
  172. * @param row old row in the Table's row format, including all values used
  173. * in any foreign-key relationships
  174. */
  175. public void deleteRow(Object[] row) throws IOException {
  176. if(!enforcing()) {
  177. return;
  178. }
  179. initialize();
  180. // first, check the tables for which we are the primary table in the
  181. // relationship (but not cascading)
  182. for(Joiner joiner : _primaryJoinersChkDel) {
  183. requireNoSecondaryValues(joiner, row);
  184. }
  185. // lastly, delete from the tables for which we are the primary table in
  186. // the relationship
  187. for(Joiner joiner : _primaryJoinersDoDel) {
  188. joiner.deleteRows(row);
  189. }
  190. }
  191. private static void requirePrimaryValues(Joiner joiner, Object[] row)
  192. throws IOException
  193. {
  194. // ensure that the relevant rows exist in the primary tables for which
  195. // this table is a secondary table.
  196. if(!joiner.hasRows(row)) {
  197. throw new IOException("Adding new row " + Arrays.asList(row) +
  198. " violates constraint " + joiner.toFKString());
  199. }
  200. }
  201. private static void requireNoSecondaryValues(Joiner joiner, Object[] row)
  202. throws IOException
  203. {
  204. // ensure that no rows exist in the secondary table for which this table is
  205. // the primary table.
  206. if(joiner.hasRows(row)) {
  207. throw new IOException("Removing old row " + Arrays.asList(row) +
  208. " violates constraint " + joiner.toFKString());
  209. }
  210. }
  211. private static void updateSecondaryValues(Joiner joiner, Object[] oldFromRow,
  212. Object[] newFromRow)
  213. throws IOException
  214. {
  215. IndexCursor toCursor = joiner.getToCursor();
  216. List<? extends Index.Column> fromCols = joiner.getColumns();
  217. List<? extends Index.Column> toCols = joiner.getToIndex().getColumns();
  218. Object[] toRow = new Object[joiner.getToTable().getColumnCount()];
  219. for(Iterator<Row> iter = joiner.findRows(oldFromRow)
  220. .setColumnNames(Collections.<String>emptySet())
  221. .iterator(); iter.hasNext(); ) {
  222. iter.next();
  223. // create update row for "to" table
  224. Arrays.fill(toRow, Column.KEEP_VALUE);
  225. for(int i = 0; i < fromCols.size(); ++i) {
  226. Object val = fromCols.get(i).getColumn().getRowValue(newFromRow);
  227. toCols.get(i).getColumn().setRowValue(toRow, val);
  228. }
  229. toCursor.updateCurrentRow(toRow);
  230. }
  231. }
  232. private boolean anyUpdates(Object[] oldRow, Object[] newRow) {
  233. for(ColumnImpl col : _cols) {
  234. if(!MATCHER.matches(_table, col.getName(),
  235. col.getRowValue(oldRow), col.getRowValue(newRow))) {
  236. return true;
  237. }
  238. }
  239. return false;
  240. }
  241. private static boolean anyUpdates(Joiner joiner,Object[] oldRow,
  242. Object[] newRow)
  243. {
  244. Table fromTable = joiner.getFromTable();
  245. for(Index.Column iCol : joiner.getColumns()) {
  246. Column col = iCol.getColumn();
  247. if(!MATCHER.matches(fromTable, col.getName(),
  248. col.getRowValue(oldRow), col.getRowValue(newRow))) {
  249. return true;
  250. }
  251. }
  252. return false;
  253. }
  254. private boolean enforcing() {
  255. return _table.getDatabase().isEnforceForeignKeys();
  256. }
  257. static SharedState initSharedState() {
  258. return new SharedState();
  259. }
  260. /**
  261. * Shared state used by all FKEnforcers for a given Database.
  262. */
  263. static final class SharedState
  264. {
  265. /** current depth of cascading update calls across one or more tables */
  266. private int _updateDepth;
  267. private SharedState() {
  268. }
  269. public boolean isUpdating() {
  270. return (_updateDepth == 0);
  271. }
  272. public void pushUpdate() {
  273. ++_updateDepth;
  274. }
  275. public void popUpdate() {
  276. --_updateDepth;
  277. }
  278. }
  279. }