/*****************************************************************
* 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.Cayenne;
import org.apache.cayenne.DataObject;
import org.apache.cayenne.DataRow;
import org.apache.cayenne.Fault;
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.map.EntityResolver;
import org.apache.cayenne.query.EJBQLQuery;
import org.apache.cayenne.query.ObjectIdQuery;
import org.apache.cayenne.query.Query;
import org.apache.cayenne.query.QueryMetadata;
import org.apache.cayenne.query.QueryRouter;
import org.apache.cayenne.query.SQLTemplate;
import org.apache.cayenne.query.SelectQuery;
import org.apache.cayenne.test.jdbc.DBHelper;
import org.apache.cayenne.test.jdbc.TableHelper;
import org.apache.cayenne.testdo.testmap.Artist;
import org.apache.cayenne.testdo.testmap.Exhibit;
import org.apache.cayenne.testdo.testmap.NullTestEntity;
import org.apache.cayenne.testdo.testmap.Painting;
import org.apache.cayenne.testdo.testmap.ROArtist;
import org.apache.cayenne.unit.UnitDbAdapter;
import org.apache.cayenne.unit.di.DataChannelInterceptor;
import org.apache.cayenne.unit.di.UnitTestClosure;
import org.apache.cayenne.unit.di.server.CayenneProjects;
import org.apache.cayenne.unit.di.server.ServerCase;
import org.apache.cayenne.unit.di.server.ServerCaseDataSourceFactory;
import org.apache.cayenne.unit.di.server.UseServerRuntime;
import org.junit.Before;
import org.junit.Test;
import java.math.BigDecimal;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static org.junit.Assert.*;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.eq;
import static org.mockito.Matchers.isNull;
import static org.mockito.Mockito.*;
@UseServerRuntime(CayenneProjects.TESTMAP_PROJECT)
public class DataContextIT extends ServerCase {
@Inject
protected DataContext context;
@Inject
protected DBHelper dbHelper;
@Inject
protected UnitDbAdapter accessStackAdapter;
@Inject
protected DataChannelInterceptor queryInterceptor;
@Inject
protected ServerCaseDataSourceFactory dataSourceFactory;
protected TableHelper tArtist;
protected TableHelper tExhibit;
protected TableHelper tGallery;
protected TableHelper tPainting;
@Before
public void setUp() throws Exception {
tArtist = new TableHelper(dbHelper, "ARTIST");
tArtist.setColumns("ARTIST_ID", "ARTIST_NAME");
tExhibit = new TableHelper(dbHelper, "EXHIBIT");
tExhibit.setColumns("EXHIBIT_ID", "GALLERY_ID", "OPENING_DATE", "CLOSING_DATE");
tGallery = new TableHelper(dbHelper, "GALLERY");
tGallery.setColumns("GALLERY_ID", "GALLERY_NAME");
tPainting = new TableHelper(dbHelper, "PAINTING");
tPainting.setColumns("PAINTING_ID", "PAINTING_TITLE", "ARTIST_ID", "ESTIMATED_PRICE");
}
protected void createSingleArtistDataSet() throws Exception {
tArtist.insert(33001, "artist1");
}
protected void createFiveArtistDataSet_MixedCaseName() throws Exception {
tArtist.insert(33001, "artist1");
tArtist.insert(33002, "Artist3");
tArtist.insert(33003, "aRtist5");
tArtist.insert(33004, "arTist2");
tArtist.insert(33005, "artISt4");
}
protected void createGalleriesAndExhibitsDataSet() throws Exception {
tGallery.insert(33001, "gallery1");
tGallery.insert(33002, "gallery2");
tGallery.insert(33003, "gallery3");
tGallery.insert(33004, "gallery4");
Timestamp now = new Timestamp(System.currentTimeMillis());
tExhibit.insert(1, 33001, now, now);
tExhibit.insert(2, 33002, now, now);
}
protected void createArtistsDataSet() throws Exception {
tArtist.insert(33001, "artist1");
tArtist.insert(33002, "artist2");
tArtist.insert(33003, "artist3");
tArtist.insert(33004, "artist4");
tArtist.insert(33005, "artist5");
tArtist.insert(33006, "artist11");
tArtist.insert(33007, "artist21");
}
protected void createArtistsAndPaintingsDataSet() throws Exception {
createArtistsDataSet();
tPainting.insert(33001, "P_artist1", 33001, 1000);
tPainting.insert(33002, "P_artist2", 33002, 2000);
tPainting.insert(33003, "P_artist3", 33003, 3000);
tPainting.insert(33004, "P_artist4", 33004, 4000);
tPainting.insert(33005, "P_artist5", 33005, 5000);
tPainting.insert(33006, "P_artist11", 33006, 11000);
tPainting.insert(33007, "P_artist21", 33007, 21000);
}
@Test
public void testCurrentSnapshot1() throws Exception {
createSingleArtistDataSet();
SelectQuery query = new SelectQuery(Artist.class, Artist.ARTIST_NAME.eq("artist1"));
Artist artist = (Artist) context.performQuery(query).get(0);
DataRow snapshot = context.currentSnapshot(artist);
assertEquals(artist.getArtistName(), snapshot.get("ARTIST_NAME"));
assertEquals(artist.getDateOfBirth(), snapshot.get("DATE_OF_BIRTH"));
assertEquals("Artist", snapshot.getEntityName());
}
@Test
public void testCurrentSnapshot2() throws Exception {
createSingleArtistDataSet();
// test null values
SelectQuery<Artist> query = new SelectQuery<>(Artist.class, Artist.ARTIST_NAME.eq("artist1"));
Artist artist = (Artist) context.performQuery(query).get(0);
artist.setArtistName(null);
artist.setDateOfBirth(null);
DataRow snapshot = context.currentSnapshot(artist);
assertEquals("Artist", snapshot.getEntityName());
assertTrue(snapshot.containsKey("ARTIST_NAME"));
assertNull(snapshot.get("ARTIST_NAME"));
assertTrue(snapshot.containsKey("DATE_OF_BIRTH"));
assertNull(snapshot.get("DATE_OF_BIRTH"));
}
@Test
public void testCurrentSnapshot3() throws Exception {
createSingleArtistDataSet();
// test null values
SelectQuery query = new SelectQuery(Artist.class, Artist.ARTIST_NAME.eq("artist1"));
Artist artist = (Artist) context.performQuery(query).get(0);
// test FK relationship snapshotting
Painting p1 = new Painting();
context.registerNewObject(p1);
p1.setToArtist(artist);
DataRow s1 = context.currentSnapshot(p1);
assertEquals("Painting", s1.getEntityName());
Map<String, Object> idMap = artist.getObjectId().getIdSnapshot();
assertEquals(idMap.get("ARTIST_ID"), s1.get("ARTIST_ID"));
}
/**
* Testing snapshot with to-one fault. This was a bug CAY-96.
*/
@Test
public void testCurrentSnapshotWithToOneFault() throws Exception {
createGalleriesAndExhibitsDataSet();
// Exhibit with Gallery as Fault must still include Gallery
// Artist and Exhibit (Exhibit has unresolved to-one to gallery as in
// the
// CAY-96 bug report)
ObjectId eId = new ObjectId("Exhibit", Exhibit.EXHIBIT_ID_PK_COLUMN, 2);
Exhibit e = (Exhibit) context.performQuery(new ObjectIdQuery(eId)).get(0);
assertTrue(e.readPropertyDirectly(Exhibit.TO_GALLERY.getName()) instanceof Fault);
DataRow snapshot = context.currentSnapshot(e);
// assert that after taking a snapshot, we have FK in, but the
// relationship
// is still a Fault
assertTrue(e.readPropertyDirectly(Exhibit.TO_GALLERY.getName()) instanceof Fault);
assertEquals(new Integer(33002), snapshot.get("GALLERY_ID"));
}
/**
* Tests how CHAR field is handled during fetch. Some databases (Oracle...)
* would pad a CHAR column with extra spaces, returned to the client.
* Cayenne should trim it.
*/
@Test
public void testCharFetch() throws Exception {
createSingleArtistDataSet();
SelectQuery query = new SelectQuery(Artist.class);
Artist a = (Artist) context.performQuery(query).get(0);
assertEquals(a.getArtistName().trim(), a.getArtistName());
}
/**
* Tests how CHAR field is handled during fetch in the WHERE clause. Some
* databases (Oracle...) would pad a CHAR column with extra spaces, returned
* to the client. Cayenne should trim it.
*/
@Test
public void testCharInQualifier() throws Exception {
createArtistsDataSet();
Expression e = ExpressionFactory.matchExp("artistName", "artist1");
SelectQuery q = new SelectQuery(Artist.class, e);
List<Artist> artists = context.performQuery(q);
assertEquals(1, artists.size());
}
/**
* Test fetching query with multiple relationship paths between the same 2
* entities used in qualifier.
*/
@Test
public void testMultiObjRelFetch() throws Exception {
createArtistsAndPaintingsDataSet();
SelectQuery q = new SelectQuery(Painting.class);
q.andQualifier(ExpressionFactory.matchExp("toArtist.artistName", "artist2"));
q.orQualifier(ExpressionFactory.matchExp("toArtist.artistName", "artist4"));
List<Painting> results = context.performQuery(q);
assertEquals(2, results.size());
}
/**
* Test fetching query with multiple relationship paths between the same 2
* entities used in qualifier.
*/
@Test
public void testMultiDbRelFetch() throws Exception {
createArtistsAndPaintingsDataSet();
SelectQuery q = new SelectQuery("Painting");
q.andQualifier(ExpressionFactory.matchDbExp("toArtist.ARTIST_NAME", "artist2"));
q.orQualifier(ExpressionFactory.matchDbExp("toArtist.ARTIST_NAME", "artist4"));
List<?> results = context.performQuery(q);
assertEquals(2, results.size());
}
@Test
public void testSelectDate() throws Exception {
createGalleriesAndExhibitsDataSet();
List<Exhibit> objects = context.performQuery(new SelectQuery(Exhibit.class));
assertFalse(objects.isEmpty());
Exhibit e1 = objects.get(0);
assertEquals(java.util.Date.class, e1.getClosingDate().getClass());
}
@Test
public void testCaseInsensitiveOrdering() throws Exception {
if (!accessStackAdapter.supportsCaseInsensitiveOrder()) {
return;
}
createFiveArtistDataSet_MixedCaseName();
// case insensitive ordering appends extra columns
// to the query when query is using DISTINCT...
// verify that the result is not messed up
SelectQuery query = new SelectQuery(Artist.class);
query.addOrdering(Artist.ARTIST_NAME.ascInsensitive());
query.setDistinct(true);
List<Artist> objects = context.performQuery(query);
assertEquals(5, objects.size());
Artist artist = objects.get(0);
DataRow snapshot = context.getObjectStore().getSnapshot(artist.getObjectId());
assertEquals(3, snapshot.size());
// assert the ordering
assertEquals("artist1", objects.get(0).getArtistName());
assertEquals("arTist2", objects.get(1).getArtistName());
assertEquals("Artist3", objects.get(2).getArtistName());
assertEquals("artISt4", objects.get(3).getArtistName());
assertEquals("aRtist5", objects.get(4).getArtistName());
}
@Test
public void testSelect_DataRows() throws Exception {
createArtistsAndPaintingsDataSet();
SelectQuery<DataRow> query = SelectQuery.dataRowQuery(Artist.class, null);
List<DataRow> objects = context.select(query);
assertNotNull(objects);
assertEquals(7, objects.size());
assertTrue("DataRow expected, got " + objects.get(0).getClass(), objects.get(0) instanceof DataRow);
}
@Test
public void testPerformSelectQuery1() throws Exception {
createArtistsAndPaintingsDataSet();
SelectQuery query = new SelectQuery(Artist.class);
List<?> objects = context.performQuery(query);
assertNotNull(objects);
assertEquals(7, objects.size());
assertTrue("Artist expected, got " + objects.get(0).getClass(), objects.get(0) instanceof Artist);
}
@Test
public void testPerformSelectQuery2() throws Exception {
createArtistsAndPaintingsDataSet();
// do a query with complex qualifier
List<Expression> expressions = new ArrayList<Expression>();
expressions.add(ExpressionFactory.matchExp("artistName", "artist3"));
expressions.add(ExpressionFactory.matchExp("artistName", "artist5"));
expressions.add(ExpressionFactory.matchExp("artistName", "artist21"));
SelectQuery query = new SelectQuery(Artist.class, ExpressionFactory.joinExp(Expression.OR, expressions));
List<?> objects = context.performQuery(query);
assertNotNull(objects);
assertEquals(3, objects.size());
assertTrue("Artist expected, got " + objects.get(0).getClass(), objects.get(0) instanceof Artist);
}
@Test
public void testPerformQuery_Routing() {
Query query = mock(Query.class);
QueryMetadata md = mock(QueryMetadata.class);
when(query.getMetaData(any(EntityResolver.class))).thenReturn(md);
context.performGenericQuery(query);
verify(query).route(any(QueryRouter.class), eq(context.getEntityResolver()), (Query) isNull());
}
@Test
public void testPerformNonSelectingQuery() throws Exception {
createSingleArtistDataSet();
SelectQuery select = new SelectQuery(Painting.class, ExpressionFactory.exp("db:PAINTING_ID = 1"));
assertEquals(0, context.performQuery(select).size());
SQLTemplate query = new SQLTemplate(Painting.class,
"INSERT INTO PAINTING (PAINTING_ID, PAINTING_TITLE, ARTIST_ID, ESTIMATED_PRICE) "
+ "VALUES (1, 'PX', 33001, 1)");
context.performNonSelectingQuery(query);
assertEquals(1, context.performQuery(select).size());
}
@Test
public void testPerformNonSelectingQueryCounts1() throws Exception {
createArtistsDataSet();
SQLTemplate query = new SQLTemplate(Painting.class,
"INSERT INTO PAINTING (PAINTING_ID, PAINTING_TITLE, ARTIST_ID, ESTIMATED_PRICE) "
+ "VALUES ($pid, '$pt', $aid, $price)");
Map<String, Object> map = new HashMap<>();
map.put("pid", new Integer(1));
map.put("pt", "P1");
map.put("aid", new Integer(33002));
map.put("price", new Double(1.1));
// single batch of parameters
query.setParameters(map);
int[] counts = context.performNonSelectingQuery(query);
assertNotNull(counts);
assertEquals(1, counts.length);
assertEquals(1, counts[0]);
}
@Test
public void testPerformNonSelectingQueryCounts2() throws Exception {
createArtistsDataSet();
SQLTemplate query = new SQLTemplate(Painting.class,
"INSERT INTO PAINTING (PAINTING_ID, PAINTING_TITLE, ARTIST_ID, ESTIMATED_PRICE) "
+ "VALUES ($pid, '$pt', $aid, #bind($price 'DECIMAL' 2))");
Map<String, Object>[] maps = new Map[3];
for (int i = 0; i < maps.length; i++) {
maps[i] = new HashMap<>();
maps[i].put("pid", new Integer(1 + i));
maps[i].put("pt", "P-" + i);
maps[i].put("aid", new Integer(33002));
maps[i].put("price", new BigDecimal("1." + i));
}
// single batch of parameters
query.setParameters(maps);
int[] counts = context.performNonSelectingQuery(query);
assertNotNull(counts);
assertEquals(maps.length, counts.length);
for (int i = 0; i < maps.length; i++) {
assertEquals(1, counts[i]);
}
SQLTemplate delete = new SQLTemplate(Painting.class, "delete from PAINTING");
counts = context.performNonSelectingQuery(delete);
assertNotNull(counts);
assertEquals(1, counts.length);
assertEquals(3, counts[0]);
}
@Test
public void testPerformPaginatedQuery() throws Exception {
createArtistsDataSet();
SelectQuery<Artist> query = SelectQuery.query(Artist.class);
query.setPageSize(5);
List<Artist> objects = context.select(query);
assertNotNull(objects);
assertTrue(objects instanceof IncrementalFaultList<?>);
assertTrue(((IncrementalFaultList<Artist>) objects).elements.get(0) instanceof Long);
assertTrue(((IncrementalFaultList<Artist>) objects).elements.get(6) instanceof Long);
assertTrue(objects.get(0) instanceof Artist);
}
@Test
public void testPerformPaginatedQuery1() throws Exception {
createArtistsDataSet();
EJBQLQuery query = new EJBQLQuery("select a FROM Artist a");
query.setPageSize(5);
List<?> objects = context.performQuery(query);
assertNotNull(objects);
assertTrue(objects instanceof IncrementalFaultList<?>);
assertTrue(((IncrementalFaultList<?>) objects).elements.get(0) instanceof Long);
assertTrue(((IncrementalFaultList<?>) objects).elements.get(6) instanceof Long);
assertTrue(objects.get(0) instanceof Artist);
}
@Test
public void testPerformPaginatedQueryBigPage() throws Exception {
createArtistsDataSet();
SelectQuery query = new SelectQuery(Artist.class);
query.setPageSize(5);
final List<?> objects = context.performQuery(query);
assertNotNull(objects);
assertTrue(objects instanceof IncrementalFaultList<?>);
queryInterceptor.runWithQueriesBlocked(new UnitTestClosure() {
public void execute() {
assertEquals(7, objects.size());
}
});
}
@Test
public void testPerformDataRowQuery() throws Exception {
createArtistsDataSet();
SelectQuery query = new SelectQuery(Artist.class);
query.setFetchingDataRows(true);
List<?> objects = context.performQuery(query);
assertNotNull(objects);
assertEquals(7, objects.size());
assertTrue("Map expected, got " + objects.get(0).getClass(), objects.get(0) instanceof Map<?, ?>);
}
@Test
public void testCommitChangesRO1() throws Exception {
ROArtist a1 = (ROArtist) context.newObject("ROArtist");
a1.writePropertyDirectly("artistName", "abc");
a1.setPersistenceState(PersistenceState.MODIFIED);
try {
context.commitChanges();
fail("Inserting a 'read-only' object must fail.");
} catch (Exception ex) {
// exception is expected,
// must blow on saving new "read-only" object.
}
}
@Test
public void testCommitChangesRO2() throws Exception {
createArtistsDataSet();
SelectQuery query = new SelectQuery(ROArtist.class, Artist.ARTIST_NAME.eq("artist1"));
ROArtist a1 = (ROArtist) context.performQuery(query).get(0);
a1.writeProperty(ROArtist.ARTIST_NAME.getName(), "abc");
try {
context.commitChanges();
fail("Updating a 'read-only' object must fail.");
} catch (Exception ex) {
// exception is expected,
// must blow on saving new "read-only" object.
}
}
@Test
public void testCommitChangesRO3() throws Exception {
createArtistsDataSet();
SelectQuery query = new SelectQuery(ROArtist.class, Artist.ARTIST_NAME.eq("artist1"));
ROArtist a1 = (ROArtist) context.performQuery(query).get(0);
context.deleteObjects(a1);
try {
context.commitChanges();
fail("Deleting a 'read-only' object must fail.");
} catch (Exception ex) {
// exception is expected,
// must blow on saving new "read-only" object.
}
}
@Test
public void testCommitChangesRO4() throws Exception {
createArtistsDataSet();
SelectQuery query = new SelectQuery(ROArtist.class, Artist.ARTIST_NAME.eq("artist1"));
ROArtist a1 = (ROArtist) context.performQuery(query).get(0);
Painting painting = context.newObject(Painting.class);
painting.setPaintingTitle("paint");
a1.addToPaintingArray(painting);
assertEquals(PersistenceState.MODIFIED, a1.getPersistenceState());
try {
context.commitChanges();
} catch (Exception ex) {
fail("Updating 'read-only' object's to-many must succeed, instead an exception was thrown: " + ex);
}
assertEquals(PersistenceState.COMMITTED, a1.getPersistenceState());
}
/**
* Tests that hasChanges performs correctly when an object is "modified" and
* the property is simply set to the same value (an unreal modification)
*/
@Test
public void testHasChangesPhantom() {
String artistName = "ArtistName";
Artist artist = (Artist) context.newObject("Artist");
artist.setArtistName(artistName);
context.commitChanges();
// Set again to *exactly* the same value
artist.setArtistName(artistName);
// note that since 1.2 the polciy is for hasChanges to return true for
// phantom
// modifications, as there is no way to detect some more subtle
// modifications like
// a change of the master related object, until we actually create the
// PKs
assertTrue(context.hasChanges());
}
/**
* Tests that hasChanges performs correctly when an object is "modified" and
* the property is simply set to the same value (an unreal modification)
*/
@Test
public void testHasChangesRealModify() {
Artist artist = (Artist) context.newObject("Artist");
artist.setArtistName("ArtistName");
context.commitChanges();
artist.setArtistName("Something different");
assertTrue(context.hasChanges());
}
@Test
public void testInvalidateObjects_Vararg() throws Exception {
DataRow row = new DataRow(10);
row.put("ARTIST_ID", new Integer(1));
row.put("ARTIST_NAME", "ArtistXYZ");
row.put("DATE_OF_BIRTH", new Date());
DataObject object = context.objectFromDataRow(Artist.class, row);
ObjectId oid = object.getObjectId();
// insert object into the ObjectStore
context.getObjectStore().registerNode(oid, object);
assertSame(object, context.getObjectStore().getNode(oid));
assertNotNull(context.getObjectStore().getCachedSnapshot(oid));
context.invalidateObjects(object);
assertSame(oid, object.getObjectId());
assertNull(context.getObjectStore().getCachedSnapshot(oid));
assertSame(object, context.getObjectStore().getNode(oid));
}
@Test
public void testInvalidateObjects() throws Exception {
DataRow row = new DataRow(10);
row.put("ARTIST_ID", new Integer(1));
row.put("ARTIST_NAME", "ArtistXYZ");
row.put("DATE_OF_BIRTH", new Date());
DataObject object = context.objectFromDataRow(Artist.class, row);
ObjectId oid = object.getObjectId();
// insert object into the ObjectStore
context.getObjectStore().registerNode(oid, object);
assertSame(object, context.getObjectStore().getNode(oid));
assertNotNull(context.getObjectStore().getCachedSnapshot(oid));
context.invalidateObjects(Collections.singleton(object));
assertSame(oid, object.getObjectId());
assertNull(context.getObjectStore().getCachedSnapshot(oid));
assertSame(object, context.getObjectStore().getNode(oid));
}
@Test
public void testBeforeHollowDeleteShouldChangeStateToCommited() throws Exception {
createSingleArtistDataSet();
Artist hollow = Cayenne.objectForPK(context, Artist.class, 33001);
context.invalidateObjects(hollow);
assertEquals(PersistenceState.HOLLOW, hollow.getPersistenceState());
// testing this...
context.deleteObjects(hollow);
assertSame(hollow, context.getGraphManager().getNode(new ObjectId("Artist", "ARTIST_ID", 33001)));
assertEquals("artist1", hollow.getArtistName());
assertEquals(PersistenceState.DELETED, hollow.getPersistenceState());
}
@Test
public void testCommitUnchangedInsert() throws Exception {
// see CAY-1444 - reproducible on DB's that support auto incremented PK
NullTestEntity newObject = context.newObject(NullTestEntity.class);
assertTrue(context.hasChanges());
context.commitChanges();
assertFalse(context.hasChanges());
assertEquals(PersistenceState.COMMITTED, newObject.getPersistenceState());
}
}