1 /*- 2 * Copyright (c) 2000, 2020 Oracle and/or its affiliates. All rights reserved. 3 * 4 * See the file LICENSE for license information. 5 * 6 */ 7 8 package com.sleepycat.persist.test; 9 10 import static com.sleepycat.persist.model.DeleteAction.ABORT; 11 import static com.sleepycat.persist.model.DeleteAction.CASCADE; 12 import static com.sleepycat.persist.model.DeleteAction.NULLIFY; 13 import static com.sleepycat.persist.model.Relationship.ONE_TO_ONE; 14 import static org.junit.Assert.assertEquals; 15 import static org.junit.Assert.assertNotNull; 16 import static org.junit.Assert.assertNull; 17 import static org.junit.Assert.assertTrue; 18 import static org.junit.Assert.fail; 19 20 import java.util.ArrayList; 21 import java.util.List; 22 23 import org.junit.Test; 24 import org.junit.runner.RunWith; 25 import org.junit.runners.Parameterized; 26 import org.junit.runners.Parameterized.Parameters; 27 28 import com.sleepycat.compat.DbCompat; 29 import com.sleepycat.db.DatabaseException; 30 import com.sleepycat.db.Transaction; 31 import com.sleepycat.persist.EntityStore; 32 import com.sleepycat.persist.PrimaryIndex; 33 import com.sleepycat.persist.SecondaryIndex; 34 import com.sleepycat.persist.StoreConfig; 35 import com.sleepycat.persist.model.DeleteAction; 36 import com.sleepycat.persist.model.Entity; 37 import com.sleepycat.persist.model.Persistent; 38 import com.sleepycat.persist.model.PrimaryKey; 39 import com.sleepycat.persist.model.SecondaryKey; 40 import com.sleepycat.util.test.TxnTestCase; 41 42 /** 43 * @author Mark Hayes 44 */ 45 @RunWith(Parameterized.class) 46 public class ForeignKeyTest extends TxnTestCase { 47 48 protected static final DeleteAction[] ACTIONS = { 49 ABORT, 50 NULLIFY, 51 CASCADE, 52 }; 53 54 protected static final String[] ACTION_LABELS = { 55 "ABORT", 56 "NULLIFY", 57 "CASCADE", 58 }; 59 60 @Parameters genParams()61 public static List<Object[]> genParams() { 62 return paramsHelper(false); 63 } 64 paramsHelper(boolean rep)65 protected static List<Object[]> paramsHelper(boolean rep) { 66 final String[] txnTypes = getTxnTypes(null, rep); 67 final List<Object[]> newParams = new ArrayList<Object[]>(); 68 int i = 0; 69 for (final DeleteAction action : ACTIONS) { 70 for (final String type : txnTypes) { 71 newParams.add(new Object[] 72 {type, action, ACTION_LABELS[i], "UseSubclass"}); 73 newParams.add(new Object[] 74 {type, action, ACTION_LABELS[i], "UseBaseclass"}); 75 } 76 i++; 77 } 78 return newParams; 79 } 80 ForeignKeyTest(String type, DeleteAction action, String label, String useClassLabel)81 public ForeignKeyTest(String type, 82 DeleteAction action, 83 String label, 84 String useClassLabel){ 85 initEnvConfig(); 86 txnType = type; 87 isTransactional = (txnType != TXN_NULL); 88 onDelete = action; 89 onDeleteLabel = label; 90 useSubclassLabel = useClassLabel; 91 customName = txnType + '-' + onDeleteLabel + "-" + useSubclassLabel; 92 } 93 94 private EntityStore store; 95 private PrimaryIndex<String, Entity1> pri1; 96 private PrimaryIndex<String, Entity2> pri2; 97 private SecondaryIndex<String, String, Entity1> sec1; 98 private SecondaryIndex<String, String, Entity2> sec2; 99 private final DeleteAction onDelete; 100 private final String onDeleteLabel; 101 private boolean useSubclass; 102 private final String useSubclassLabel; 103 open()104 private void open() 105 throws DatabaseException { 106 107 StoreConfig config = new StoreConfig(); 108 config.setAllowCreate(envConfig.getAllowCreate()); 109 config.setTransactional(envConfig.getTransactional()); 110 111 store = new EntityStore(env, "test", config); 112 113 pri1 = store.getPrimaryIndex(String.class, Entity1.class); 114 sec1 = store.getSecondaryIndex(pri1, String.class, "sk"); 115 pri2 = store.getPrimaryIndex(String.class, Entity2.class); 116 sec2 = store.getSecondaryIndex 117 (pri2, String.class, "sk_" + onDeleteLabel); 118 } 119 close()120 private void close() 121 throws DatabaseException { 122 123 store.close(); 124 } 125 126 @Test testForeignKeys()127 public void testForeignKeys() 128 throws Exception { 129 130 open(); 131 Transaction txn = txnBegin(); 132 133 Entity1 o1 = new Entity1("pk1", "sk1"); 134 assertNull(pri1.put(txn, o1)); 135 136 assertEquals(o1, pri1.get(txn, "pk1", null)); 137 assertEquals(o1, sec1.get(txn, "sk1", null)); 138 139 Entity2 o2 = (useSubclass ? 140 new Entity3("pk2", "pk1", onDelete) : 141 new Entity2("pk2", "pk1", onDelete)); 142 assertNull(pri2.put(txn, o2)); 143 144 assertEquals(o2, pri2.get(txn, "pk2", null)); 145 assertEquals(o2, sec2.get(txn, "pk1", null)); 146 147 txnCommit(txn); 148 txn = txnBegin(); 149 150 /* 151 * pri1 contains o1 with primary key "pk1" and index key "sk1". 152 * 153 * pri2 contains o2 with primary key "pk2" and foreign key "pk1", 154 * which is the primary key of pri1. 155 */ 156 if (onDelete == ABORT) { 157 158 /* Test that we abort trying to delete a referenced key. */ 159 160 try { 161 pri1.delete(txn, "pk1"); 162 fail(); 163 } catch (DatabaseException expected) { 164 assertTrue(!DbCompat.NEW_JE_EXCEPTIONS); 165 txnAbort(txn); 166 txn = txnBegin(); 167 } 168 169 /* 170 * Test that we can put a record into store2 with a null foreign 171 * key value. 172 */ 173 o2 = (useSubclass ? 174 new Entity3("pk2", null, onDelete) : 175 new Entity2("pk2", null, onDelete)); 176 assertNotNull(pri2.put(txn, o2)); 177 assertEquals(o2, pri2.get(txn, "pk2", null)); 178 179 /* 180 * The index2 record should have been deleted since the key was set 181 * to null above. 182 */ 183 assertNull(sec2.get(txn, "pk1", null)); 184 185 /* 186 * Test that now we can delete the record in store1, since it is no 187 * longer referenced. 188 */ 189 assertNotNull(pri1.delete(txn, "pk1")); 190 assertNull(pri1.get(txn, "pk1", null)); 191 assertNull(sec1.get(txn, "sk1", null)); 192 193 } else if (onDelete == NULLIFY) { 194 195 /* Delete the referenced key. */ 196 assertNotNull(pri1.delete(txn, "pk1")); 197 assertNull(pri1.get(txn, "pk1", null)); 198 assertNull(sec1.get(txn, "sk1", null)); 199 200 /* 201 * The store2 record should still exist, but should have an empty 202 * secondary key since it was nullified. 203 */ 204 o2 = pri2.get(txn, "pk2", null); 205 assertNotNull(o2); 206 assertEquals("pk2", o2.pk); 207 assertEquals(null, o2.getSk(onDelete)); 208 209 } else if (onDelete == CASCADE) { 210 211 /* Delete the referenced key. */ 212 assertNotNull(pri1.delete(txn, "pk1")); 213 assertNull(pri1.get(txn, "pk1", null)); 214 assertNull(sec1.get(txn, "sk1", null)); 215 216 /* The store2 record should have deleted also. */ 217 assertNull(pri2.get(txn, "pk2", null)); 218 assertNull(sec2.get(txn, "pk1", null)); 219 220 } else { 221 throw new IllegalStateException(); 222 } 223 224 /* 225 * Test that a foreign key value may not be used that is not present in 226 * the foreign store. "pk2" is not in store1 in this case. 227 */ 228 Entity2 o3 = (useSubclass ? 229 new Entity3("pk3", "pk2", onDelete) : 230 new Entity2("pk3", "pk2", onDelete)); 231 try { 232 pri2.put(txn, o3); 233 fail(); 234 } catch (DatabaseException expected) { 235 assertTrue(!DbCompat.NEW_JE_EXCEPTIONS); 236 } 237 238 txnAbort(txn); 239 close(); 240 } 241 242 @Entity 243 static class Entity1 { 244 245 @PrimaryKey 246 String pk; 247 248 @SecondaryKey(relate=ONE_TO_ONE) 249 String sk; 250 Entity1()251 private Entity1() {} 252 Entity1(String pk, String sk)253 Entity1(String pk, String sk) { 254 this.pk = pk; 255 this.sk = sk; 256 } 257 258 @Override equals(Object other)259 public boolean equals(Object other) { 260 Entity1 o = (Entity1) other; 261 return nullOrEqual(pk, o.pk) && 262 nullOrEqual(sk, o.sk); 263 } 264 } 265 266 @Entity 267 static class Entity2 { 268 269 @PrimaryKey 270 String pk; 271 272 @SecondaryKey(relate=ONE_TO_ONE, relatedEntity=Entity1.class, 273 onRelatedEntityDelete=ABORT) 274 String sk_ABORT; 275 276 @SecondaryKey(relate=ONE_TO_ONE, relatedEntity=Entity1.class, 277 onRelatedEntityDelete=CASCADE) 278 String sk_CASCADE; 279 280 @SecondaryKey(relate=ONE_TO_ONE, relatedEntity=Entity1.class, 281 onRelatedEntityDelete=NULLIFY) 282 String sk_NULLIFY; 283 Entity2()284 private Entity2() {} 285 Entity2(String pk, String sk, DeleteAction action)286 Entity2(String pk, String sk, DeleteAction action) { 287 this.pk = pk; 288 switch (action) { 289 case ABORT: 290 sk_ABORT = sk; 291 break; 292 case CASCADE: 293 sk_CASCADE = sk; 294 break; 295 case NULLIFY: 296 sk_NULLIFY = sk; 297 break; 298 default: 299 throw new IllegalArgumentException(); 300 } 301 } 302 getSk(DeleteAction action)303 String getSk(DeleteAction action) { 304 switch (action) { 305 case ABORT: 306 return sk_ABORT; 307 case CASCADE: 308 return sk_CASCADE; 309 case NULLIFY: 310 return sk_NULLIFY; 311 default: 312 throw new IllegalArgumentException(); 313 } 314 } 315 316 @Override equals(Object other)317 public boolean equals(Object other) { 318 Entity2 o = (Entity2) other; 319 return nullOrEqual(pk, o.pk) && 320 nullOrEqual(sk_ABORT, o.sk_ABORT) && 321 nullOrEqual(sk_CASCADE, o.sk_CASCADE) && 322 nullOrEqual(sk_NULLIFY, o.sk_NULLIFY); 323 } 324 } 325 326 @Persistent 327 static class Entity3 extends Entity2 { Entity3()328 Entity3() {} 329 Entity3(String pk, String sk, DeleteAction action)330 Entity3(String pk, String sk, DeleteAction action) { 331 super(pk, sk, action); 332 } 333 } 334 nullOrEqual(Object o1, Object o2)335 static boolean nullOrEqual(Object o1, Object o2) { 336 if (o1 == null) { 337 return o2 == null; 338 } else { 339 return o1.equals(o2); 340 } 341 } 342 } 343