/*****************************************************************
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
****************************************************************/
package org.apache.cayenne.access;
import org.apache.cayenne.DataRow;
import org.apache.cayenne.ObjectId;
import org.apache.cayenne.PersistenceState;
import org.apache.cayenne.di.Inject;
import org.apache.cayenne.exp.Expression;
import org.apache.cayenne.exp.ExpressionFactory;
import org.apache.cayenne.query.SQLTemplate;
import org.apache.cayenne.query.SelectQuery;
import org.apache.cayenne.test.parallel.ParallelTestContainer;
import org.apache.cayenne.testdo.testmap.Artist;
import org.apache.cayenne.testdo.testmap.Painting;
import org.apache.cayenne.unit.di.server.CayenneProjects;
import org.apache.cayenne.unit.di.server.ServerCase;
import org.apache.cayenne.unit.di.server.UseServerRuntime;
import org.apache.cayenne.unit.util.SQLTemplateCustomizer;
import org.junit.Before;
import org.junit.Test;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNotSame;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
/**
* Test suite for testing behavior of multiple DataContexts that share the same underlying
* DataDomain.
*/
@UseServerRuntime(CayenneProjects.TESTMAP_PROJECT)
public class DataContextSharedCacheIT extends ServerCase {
@Inject
private DataContext context;
@Inject
private DataContext context1;
@Inject
private SQLTemplateCustomizer sqlTemplateCustomizer;
private Artist artist;
@Before
public void setUp() throws Exception {
// prepare a single artist record
artist = (Artist) context.newObject("Artist");
artist.setArtistName("version1");
artist.setDateOfBirth(new Date());
context.commitChanges();
}
/**
* Test case to prove that refreshing snapshots as a result of the database fetch will
* be propagated across DataContexts.
*/
@Test
public void testSnapshotChangePropagationOnSelect() throws Exception {
String originalName = artist.getArtistName();
final String newName = "version2";
// update artist using raw SQL
SQLTemplate query = sqlTemplateCustomizer.createSQLTemplate(
Artist.class,
"UPDATE ARTIST SET ARTIST_NAME = #bind($newName) "
+ "WHERE ARTIST_NAME = #bind($oldName)");
Map<String, Object> map = new HashMap<>(3);
map.put("newName", newName);
map.put("oldName", originalName);
query.setParams(map);
context.performNonSelectingQuery(query);
// fetch updated artist into the new context, and see if the original
// one gets updated
Expression qual = ExpressionFactory.matchExp("artistName", newName);
List artists = context1.performQuery(new SelectQuery<>(Artist.class, qual));
assertEquals(1, artists.size());
Artist altArtist = (Artist) artists.get(0);
// check underlying cache
DataRow freshSnapshot = context
.getObjectStore()
.getDataRowCache()
.getCachedSnapshot(altArtist.getObjectId());
assertNotNull(freshSnapshot);
assertEquals(newName, freshSnapshot.get("ARTIST_NAME"));
// check both artists
assertEquals(newName, altArtist.getArtistName());
ParallelTestContainer helper = new ParallelTestContainer() {
@Override
protected void assertResult() throws Exception {
assertEquals(
"Peer object state wasn't refreshed on fetch",
newName,
artist.getArtistName());
}
};
helper.runTest(3000);
}
/**
* Test case to prove that changes made to an object in one ObjectStore and committed
* to the database will be reflected in the peer ObjectStore using the same
* DataRowCache.
*/
@Test
public void testSnapshotChangePropagation() throws Exception {
String originalName = artist.getArtistName();
final String newName = "version2";
// make sure we have a fully resolved copy of an artist object
// in the second context
final Artist altArtist = context1.localObject(artist);
assertFalse(altArtist == artist);
assertEquals(originalName, altArtist.getArtistName());
assertEquals(PersistenceState.COMMITTED, altArtist.getPersistenceState());
// Update Artist
artist.setArtistName(newName);
// no changes propagated till commit...
assertEquals(originalName, altArtist.getArtistName());
context.commitChanges();
// check underlying cache
DataRow freshSnapshot = context
.getObjectStore()
.getDataRowCache()
.getCachedSnapshot(altArtist.getObjectId());
assertEquals(newName, freshSnapshot.get("ARTIST_NAME"));
// check peer artist
ParallelTestContainer helper = new ParallelTestContainer() {
@Override
protected void assertResult() throws Exception {
assertEquals(newName, altArtist.getArtistName());
}
};
helper.runTest(3000);
}
/**
* Test case to prove that changes made to an object in one ObjectStore and committed
* to the database will be correctly merged in the peer ObjectStore using the same
* DataRowCache. E.g. modified objects will be merged so that no new changes are lost.
*/
@Test
public void testSnapshotChangePropagationToModifiedObjects() throws Exception {
String originalName = artist.getArtistName();
Date originalDate = artist.getDateOfBirth();
String newName = "version2";
final Date newDate = new Date(originalDate.getTime() - 10000);
final String newAltName = "version3";
// make sure we have a fully resolved copy of an artist object
// in the second context
final Artist altArtist = context1.localObject(artist);
assertNotNull(altArtist);
assertFalse(altArtist == artist);
assertEquals(originalName, altArtist.getArtistName());
assertEquals(PersistenceState.COMMITTED, altArtist.getPersistenceState());
// Update Artist peers independently
artist.setArtistName(newName);
artist.setDateOfBirth(newDate);
altArtist.setArtistName(newAltName);
context.commitChanges();
// check underlying cache
DataRow freshSnapshot = context
.getObjectStore()
.getDataRowCache()
.getCachedSnapshot(altArtist.getObjectId());
assertEquals(newName, freshSnapshot.get("ARTIST_NAME"));
assertEquals(newDate, freshSnapshot.get("DATE_OF_BIRTH"));
// check peer artist
ParallelTestContainer helper = new ParallelTestContainer() {
@Override
protected void assertResult() throws Exception {
assertEquals(newAltName, altArtist.getArtistName());
assertEquals(newDate, altArtist.getDateOfBirth());
assertEquals(PersistenceState.MODIFIED, altArtist.getPersistenceState());
}
};
helper.runTest(3000);
}
/**
* Test case to prove that deleting an object in one ObjectStore and committing to the
* database will be reflected in the peer ObjectStore using the same DataRowCache. By
* default COMMITTED objects will be changed to TRANSIENT.
*/
@Test
public void testSnapshotDeletePropagationToCommitted() throws Exception {
// make sure we have a fully resolved copy of an artist object
// in the second context
final Artist altArtist = context1.localObject(artist);
assertNotNull(altArtist);
assertFalse(altArtist == artist);
assertEquals(artist.getArtistName(), altArtist.getArtistName());
assertEquals(PersistenceState.COMMITTED, altArtist.getPersistenceState());
// Update Artist
context.deleteObjects(artist);
context.commitChanges();
// check underlying cache
assertNull(context
.getObjectStore()
.getDataRowCache()
.getCachedSnapshot(altArtist.getObjectId()));
// check peer artist
ParallelTestContainer helper = new ParallelTestContainer() {
@Override
protected void assertResult() throws Exception {
assertEquals(PersistenceState.TRANSIENT, altArtist.getPersistenceState());
assertNull(altArtist.getObjectContext());
}
};
helper.runTest(3000);
}
/**
* Test case to prove that deleting an object in one ObjectStore and committed to the
* database will be reflected in the peer ObjectStore using the same DataRowCache. By
* default HOLLOW objects will be changed to TRANSIENT.
*/
@Test
public void testSnapshotDeletePropagationToHollow() throws Exception {
final Artist altArtist = context1.localObject(artist);
assertNotNull(altArtist);
assertFalse(altArtist == artist);
assertEquals(PersistenceState.HOLLOW, altArtist.getPersistenceState());
// Update Artist
context.deleteObjects(artist);
context.commitChanges();
// check underlying cache
assertNull(context
.getObjectStore()
.getDataRowCache()
.getCachedSnapshot(altArtist.getObjectId()));
// check peer artist
ParallelTestContainer helper = new ParallelTestContainer() {
@Override
protected void assertResult() throws Exception {
assertEquals(PersistenceState.TRANSIENT, altArtist.getPersistenceState());
assertNull(altArtist.getObjectContext());
}
};
helper.runTest(3000);
}
/**
* Test case to prove that deleting an object in one ObjectStore and committed to the
* database will be reflected in the peer ObjectStore using the same DataRowCache. By
* default MODIFIED objects will be changed to NEW.
*/
@Test
public void testSnapshotDeletePropagationToModified() throws Exception {
// make sure we have a fully resolved copy of an artist object
// in the second context
final Artist altArtist = context1.localObject(artist);
altArtist.getArtistName();
assertNotNull(altArtist);
assertFalse(altArtist == artist);
// modify peer
altArtist.setArtistName("version2");
assertEquals(PersistenceState.MODIFIED, altArtist.getPersistenceState());
// Update Artist
context.deleteObjects(artist);
context.commitChanges();
// check underlying cache
assertNull(context
.getObjectStore()
.getDataRowCache()
.getCachedSnapshot(altArtist.getObjectId()));
// check peer artist
ParallelTestContainer helper = new ParallelTestContainer() {
@Override
protected void assertResult() throws Exception {
assertEquals(PersistenceState.NEW, altArtist.getPersistenceState());
}
};
helper.runTest(3000);
// check if now we can save this object again, and with the original
// ObjectId
ObjectId id = altArtist.getObjectId();
assertNotNull(id);
assertNotNull(id.getIdSnapshot().get(Artist.ARTIST_ID_PK_COLUMN));
assertFalse(id.isTemporary());
context1.commitChanges();
assertEquals(PersistenceState.COMMITTED, altArtist.getPersistenceState());
}
/**
* Test case to prove that deleting an object in one ObjectStore and committing to the
* database will be reflected in the peer ObjectStore using the same DataRowCache. By
* default DELETED objects will be changed to TRANSIENT.
*/
@Test
public void testSnapshotDeletePropagationToDeleted() throws Exception {
// make sure we have a fully resolved copy of an artist object
// in the second context
final Artist altArtist = context1.localObject(artist);
altArtist.getArtistName();
assertNotNull(altArtist);
assertFalse(altArtist == artist);
// delete peer
context1.deleteObjects(altArtist);
// Update Artist
context.deleteObjects(artist);
context.commitChanges();
// check underlying cache
assertNull(context
.getObjectStore()
.getDataRowCache()
.getCachedSnapshot(altArtist.getObjectId()));
// check peer artist
ParallelTestContainer helper = new ParallelTestContainer() {
@Override
protected void assertResult() throws Exception {
assertEquals(PersistenceState.TRANSIENT, altArtist.getPersistenceState());
assertNull(altArtist.getObjectContext());
}
};
helper.runTest(3000);
assertFalse(context1.hasChanges());
}
/**
* Test case to prove that deleting an object in one ObjectStore and committing to the
* database will be reflected in the peer ObjectStore using the same DataRowCache,
* including proper processing of deleted object being held in to-many collections.
*/
@Test
public void testSnapshotDeletePropagationToManyRefresh() throws Exception {
Painting painting1 = (Painting) context.newObject("Painting");
painting1.setPaintingTitle("p1");
painting1.setToArtist(artist);
Painting painting2 = (Painting) context.newObject("Painting");
painting2.setPaintingTitle("p2");
painting2.setToArtist(artist);
context.commitChanges();
// make sure we have a fully resolved copy of an artist and painting
// objects
// in the second context
final Artist altArtist = context1.localObject(artist);
final Painting altPainting1 = context1.localObject(painting1);
final Painting altPainting2 = context1.localObject(painting2);
assertEquals(artist.getArtistName(), altArtist.getArtistName());
assertEquals(painting1.getPaintingTitle(), altPainting1.getPaintingTitle());
assertEquals(painting2.getPaintingTitle(), altPainting2.getPaintingTitle());
assertEquals(2, altArtist.getPaintingArray().size());
assertEquals(PersistenceState.COMMITTED, altArtist.getPersistenceState());
assertEquals(PersistenceState.COMMITTED, altPainting1.getPersistenceState());
assertEquals(PersistenceState.COMMITTED, altPainting2.getPersistenceState());
// make sure toOne relationships from Paintings
// are resolved...
altPainting1.getToArtist();
altPainting2.getToArtist();
assertSame(altArtist, altPainting1.readPropertyDirectly("toArtist"));
assertSame(altArtist, altPainting2.readPropertyDirectly("toArtist"));
// delete painting
context.deleteObjects(painting1);
context.commitChanges();
// check underlying cache
assertNull(context
.getObjectStore()
.getDataRowCache()
.getCachedSnapshot(painting1.getObjectId()));
// check peer artist
ParallelTestContainer helper = new ParallelTestContainer() {
@Override
protected void assertResult() throws Exception {
assertEquals(
PersistenceState.TRANSIENT,
altPainting1.getPersistenceState());
assertEquals(PersistenceState.COMMITTED, altArtist.getPersistenceState());
Collection<Painting> list = altArtist.getPaintingArray();
assertEquals(1, list.size());
assertFalse(list.contains(altPainting1));
}
};
helper.runTest(3000);
}
/**
* Test case to prove that inserting an object in one ObjectStore and committing to
* the database will be reflected in the peer ObjectStore using the same DataRowCache.
* This would mean refreshing to-many collections.
*/
@Test
public void testSnapshotInsertPropagationToManyRefresh() throws Exception {
Painting painting1 = (Painting) context.newObject("Painting");
painting1.setPaintingTitle("p1");
painting1.setToArtist(artist);
context.commitChanges();
// make sure we have a fully resolved copy of an artist and painting
// objects
// in the second context
final Artist altArtist = context1.localObject(artist);
final Painting altPainting1 = context1.localObject(painting1);
assertEquals(artist.getArtistName(), altArtist.getArtistName());
assertEquals(painting1.getPaintingTitle(), altPainting1.getPaintingTitle());
assertEquals(1, altArtist.getPaintingArray().size());
assertEquals(PersistenceState.COMMITTED, altArtist.getPersistenceState());
assertEquals(PersistenceState.COMMITTED, altPainting1.getPersistenceState());
// insert new painting and add to artist
Painting painting2 = (Painting) context.newObject("Painting");
painting2.setPaintingTitle("p2");
painting2.setToArtist(artist);
context.commitChanges();
// check peer artist
// use threaded helper as a barrier, to avoid triggering faults earlier than
// needed
ParallelTestContainer helper = new ParallelTestContainer() {
@Override
protected void assertResult() throws Exception {
Object value = altArtist.readPropertyDirectly("paintingArray");
assertTrue("Unexpected: " + value, value instanceof ToManyList);
assertTrue(((ToManyList) value).isFault());
}
};
helper.runTest(2000);
List<Painting> list = altArtist.getPaintingArray();
assertEquals(2, list.size());
}
/**
* Checks that cache is refreshed when a query "refreshingObjects" property is set to
* true.
*/
@Test
public void testCacheRefreshingOnSelect() throws Exception {
String originalName = artist.getArtistName();
final String newName = "version2";
DataContext context = (DataContext) artist.getObjectContext();
DataRow oldSnapshot = context
.getObjectStore()
.getDataRowCache()
.getCachedSnapshot(artist.getObjectId());
assertNotNull(oldSnapshot);
assertEquals(originalName, oldSnapshot.get("ARTIST_NAME"));
// update artist using raw SQL
SQLTemplate update = sqlTemplateCustomizer
.createSQLTemplate(
Artist.class,
"UPDATE ARTIST SET ARTIST_NAME = #bind($newName) WHERE ARTIST_NAME = #bind($oldName)");
Map<String, Object> map = new HashMap<>(3);
map.put("newName", newName);
map.put("oldName", originalName);
update.setParams(map);
context.performNonSelectingQuery(update);
// fetch updated artist without refreshing
Expression qual = ExpressionFactory.matchExp("artistName", newName);
SelectQuery query = new SelectQuery<>(Artist.class, qual);
List artists = context.performQuery(query);
assertEquals(1, artists.size());
artist = (Artist) artists.get(0);
// check underlying cache
DataRow freshSnapshot = context
.getObjectStore()
.getDataRowCache()
.getCachedSnapshot(artist.getObjectId());
assertNotSame(oldSnapshot, freshSnapshot);
assertEquals(newName, freshSnapshot.get("ARTIST_NAME"));
// check an artist
assertEquals(newName, artist.getArtistName());
}
@Test
public void testSnapshotEvictedForHollow() throws Exception {
String originalName = artist.getArtistName();
context.invalidateObjects(artist);
assertEquals(PersistenceState.HOLLOW, artist.getPersistenceState());
assertNull(context
.getObjectStore()
.getDataRowCache()
.getCachedSnapshot(artist.getObjectId()));
// resolve object
assertEquals(originalName, artist.getArtistName());
DataRow freshSnapshot = context
.getObjectStore()
.getDataRowCache()
.getCachedSnapshot(artist.getObjectId());
assertNotNull(freshSnapshot);
assertEquals(originalName, freshSnapshot.get("ARTIST_NAME"));
}
@Test
public void testSnapshotEvictedAndObjectsHollowedForInvalidate() throws Exception {
String originalName = artist.getArtistName();
// make sure we have a fully resolved copy of an artist object
// in the second context
final Artist altArtist = context1.localObject(artist);
context1.prepareForAccess(altArtist, null, false);
assertEquals(PersistenceState.COMMITTED, altArtist.getPersistenceState());
context.invalidateObjects(artist);
// original context
assertEquals(PersistenceState.HOLLOW, artist.getPersistenceState());
assertNull(context
.getObjectStore()
.getDataRowCache()
.getCachedSnapshot(artist.getObjectId()));
// alternate context
new ParallelTestContainer() {
@Override
protected void assertResult() throws Exception {
assertEquals(PersistenceState.HOLLOW, altArtist.getPersistenceState());
assertNull(context1
.getObjectStore()
.getDataRowCache()
.getCachedSnapshot(altArtist.getObjectId()));
}
}.runTest(5000);
// resolve object
assertEquals(originalName, altArtist.getArtistName());
DataRow altFreshSnapshot = context1
.getObjectStore()
.getDataRowCache()
.getCachedSnapshot(altArtist.getObjectId());
assertNotNull(altFreshSnapshot);
assertEquals(originalName, altFreshSnapshot.get("ARTIST_NAME"));
}
@Test
public void testSnapshotEvictedForCommitted() throws Exception {
String newName = "version2";
assertEquals(PersistenceState.COMMITTED, artist.getPersistenceState());
context.getObjectStore().getDataRowCache().forgetSnapshot(artist.getObjectId());
assertNull(context
.getObjectStore()
.getDataRowCache()
.getCachedSnapshot(artist.getObjectId()));
// modify object and try to save
artist.setArtistName(newName);
context.commitChanges();
assertEquals(newName, artist.getArtistName());
DataRow freshSnapshot = context
.getObjectStore()
.getDataRowCache()
.getCachedSnapshot(artist.getObjectId());
assertNotNull(freshSnapshot);
assertEquals(newName, freshSnapshot.get("ARTIST_NAME"));
}
@Test
public void testSnapshotEvictedForModified() throws Exception {
String newName = "version2";
assertEquals(PersistenceState.COMMITTED, artist.getPersistenceState());
// modify object PRIOR to killing the snapshot
artist.setArtistName(newName);
context.getObjectStore().getDataRowCache().forgetSnapshot(artist.getObjectId());
assertNull(context
.getObjectStore()
.getDataRowCache()
.getCachedSnapshot(artist.getObjectId()));
context.commitChanges();
assertEquals(newName, artist.getArtistName());
DataRow freshSnapshot = context
.getObjectStore()
.getDataRowCache()
.getCachedSnapshot(artist.getObjectId());
assertNotNull(freshSnapshot);
assertEquals(newName, freshSnapshot.get("ARTIST_NAME"));
}
@Test
public void testSnapshotEvictedAndChangedForModified() throws Exception {
String originalName = artist.getArtistName();
String newName = "version2";
String backendName = "version3";
assertEquals(PersistenceState.COMMITTED, artist.getPersistenceState());
// modify object PRIOR to killing the snapshot
artist.setArtistName(newName);
context.getObjectStore().getDataRowCache().forgetSnapshot(artist.getObjectId());
assertNull(context
.getObjectStore()
.getDataRowCache()
.getCachedSnapshot(artist.getObjectId()));
// now replace the row in the database
String template = "UPDATE ARTIST SET ARTIST_NAME = #bind($newName) WHERE ARTIST_NAME = #bind($oldName)";
SQLTemplate update = new SQLTemplate(Artist.class, template);
Map<String, Object> map = new HashMap<>(3);
map.put("newName", backendName);
map.put("oldName", originalName);
update.setParams(map);
context.performNonSelectingQuery(update);
context.commitChanges();
assertEquals(newName, artist.getArtistName());
DataRow freshSnapshot = context
.getObjectStore()
.getDataRowCache()
.getCachedSnapshot(artist.getObjectId());
assertNotNull(freshSnapshot);
assertEquals(newName, freshSnapshot.get("ARTIST_NAME"));
}
@Test
public void testSnapshotEvictedForDeleted() throws Exception {
// remember ObjectId
ObjectId id = artist.getObjectId();
assertEquals(PersistenceState.COMMITTED, artist.getPersistenceState());
// delete object PRIOR to killing the snapshot
context.deleteObjects(artist);
context.getObjectStore().getDataRowCache().forgetSnapshot(id);
assertNull(context.getObjectStore().getDataRowCache().getCachedSnapshot(id));
context.commitChanges();
assertEquals(PersistenceState.TRANSIENT, artist.getPersistenceState());
assertNull(context.getObjectStore().getDataRowCache().getCachedSnapshot(id));
}
}