/***************************************************************** * 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.DataObject; import org.apache.cayenne.DataRow; import org.apache.cayenne.ObjectId; import org.apache.cayenne.PersistenceState; import org.apache.cayenne.Persistent; import org.apache.cayenne.ValueHolder; import org.apache.cayenne.configuration.server.ServerRuntime; import org.apache.cayenne.di.Inject; import org.apache.cayenne.exp.Expression; import org.apache.cayenne.map.ObjAttribute; import org.apache.cayenne.map.ObjEntity; import org.apache.cayenne.query.SQLTemplate; import org.apache.cayenne.query.SelectQuery; import org.apache.cayenne.query.SortOrder; 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.Gallery; import org.apache.cayenne.testdo.testmap.Painting; 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.UseServerRuntime; import org.junit.Before; import org.junit.Test; import java.sql.Date; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Set; import static org.junit.Assert.*; /** * Tests joint prefetch handling by Cayenne access stack. */ @UseServerRuntime(CayenneProjects.TESTMAP_PROJECT) public class JointPrefetchIT extends ServerCase { @Inject protected DataContext context; @Inject protected ServerRuntime runtime; @Inject protected DataChannelInterceptor queryInterceptor; @Inject protected DBHelper dbHelper; protected TableHelper tArtist; protected TableHelper tGallery; protected TableHelper tPainting; @Before public void setUp() throws Exception { tArtist = new TableHelper(dbHelper, "ARTIST"); tArtist.setColumns("ARTIST_ID", "ARTIST_NAME"); 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", "GALLERY_ID"); } protected void createJointPrefetchDataSet1() throws Exception { tGallery.insert(33001, "G1"); tGallery.insert(33002, "G2"); tArtist.insert(33001, "artist1"); tArtist.insert(33002, "artist2"); tArtist.insert(33003, "artist3"); tPainting.insert(33001, "P_artist11", 33001, 1000, 33001); tPainting.insert(33002, "P_artist12", 33001, 2000, 33001); tPainting.insert(33003, "P_artist21", 33002, 3000, 33002); } protected void createJointPrefetchDataSet2() throws Exception { tGallery.insert(33001, "G1"); tGallery.insert(33002, "G2"); tArtist.insert(33001, "artist1"); tArtist.insert(33002, "artist2"); tArtist.insert(33003, "artist3"); tPainting.insert(33001, "P_artist11", 33001, 1000, 33001); tPainting.insert(33002, "P_artist12", 33001, 2000, 33001); tPainting.insert(33003, "P_artist21", 33002, 3000, 33002); } @Test public void testJointPrefetch_ToOne_FetchLimit() throws Exception { createJointPrefetchDataSet1(); SelectQuery q = new SelectQuery(Painting.class); q.setFetchLimit(2); q.setFetchOffset(0); q.addOrdering("db:PAINTING_ID", SortOrder.ASCENDING); q.addPrefetch(Painting.TO_ARTIST.joint()); final List<?> objects = context.performQuery(q); queryInterceptor.runWithQueriesBlocked(new UnitTestClosure() { public void execute() { assertEquals(2, objects.size()); Iterator<?> it = objects.iterator(); while (it.hasNext()) { Painting p = (Painting) it.next(); Artist target = p.getToArtist(); assertNotNull(target); assertEquals(PersistenceState.COMMITTED, target.getPersistenceState()); } } }); } @Test public void testJointPrefetch_ToMany_FetchLimit() throws Exception { createJointPrefetchDataSet1(); SelectQuery q = new SelectQuery(Artist.class); q.setFetchLimit(2); q.setFetchOffset(0); q.addOrdering("db:ARTIST_ID", SortOrder.ASCENDING); q.addPrefetch(Artist.PAINTING_ARRAY.joint()); final List<?> objects = context.performQuery(q); queryInterceptor.runWithQueriesBlocked(new UnitTestClosure() { public void execute() { // herein lies the limitation of prefetching combined with fetch limit - // we got fewer artists than we wanted assertEquals(1, objects.size()); Iterator<?> it = objects.iterator(); while (it.hasNext()) { Artist a = (Artist) it.next(); List<Painting> targets = a.getPaintingArray(); assertNotNull(targets); for (Painting p : targets) { assertEquals(PersistenceState.COMMITTED, p.getPersistenceState()); } } } }); } @Test public void testJointPrefetchDataRows() throws Exception { createJointPrefetchDataSet1(); // query with to-many joint prefetches SelectQuery q = new SelectQuery(Painting.class); q.addOrdering("db:PAINTING_ID", SortOrder.ASCENDING); q.setFetchingDataRows(true); q.addPrefetch(Painting.TO_ARTIST.joint()); final List<?> rows = context.performQuery(q); queryInterceptor.runWithQueriesBlocked(new UnitTestClosure() { public void execute() { assertEquals(3, rows.size()); // row should contain columns from both entities minus those duplicated in // a // join... int rowWidth = context .getEntityResolver() .getDbEntity("ARTIST") .getAttributes() .size() + context .getEntityResolver() .getDbEntity("PAINTING") .getAttributes() .size(); Iterator<?> it = rows.iterator(); while (it.hasNext()) { DataRow row = (DataRow) it.next(); assertEquals("" + row, rowWidth, row.size()); // assert columns presence assertTrue(row + "", row.containsKey("PAINTING_ID")); assertTrue(row + "", row.containsKey("ARTIST_ID")); assertTrue(row + "", row.containsKey("GALLERY_ID")); assertTrue(row + "", row.containsKey("PAINTING_TITLE")); assertTrue(row + "", row.containsKey("ESTIMATED_PRICE")); assertTrue(row + "", row.containsKey("toArtist.ARTIST_NAME")); assertTrue(row + "", row.containsKey("toArtist.DATE_OF_BIRTH")); } } }); } @Test public void testJointPrefetchSQLTemplate() throws Exception { createJointPrefetchDataSet1(); // correctly naming columns is the key.. SQLTemplate q = new SQLTemplate( Artist.class, "SELECT distinct " + "#result('ESTIMATED_PRICE' 'BigDecimal' '' 'paintingArray.ESTIMATED_PRICE'), " + "#result('PAINTING_TITLE' 'String' '' 'paintingArray.PAINTING_TITLE'), " + "#result('GALLERY_ID' 'int' '' 'paintingArray.GALLERY_ID'), " + "#result('PAINTING_ID' 'int' '' 'paintingArray.PAINTING_ID'), " + "#result('ARTIST_NAME' 'String'), " + "#result('DATE_OF_BIRTH' 'java.util.Date'), " + "#result('t0.ARTIST_ID' 'int' '' 'ARTIST_ID') " + "FROM ARTIST t0, PAINTING t1 " + "WHERE t0.ARTIST_ID = t1.ARTIST_ID"); q.addPrefetch(Artist.PAINTING_ARRAY.joint()); q.setFetchingDataRows(false); final List<?> objects = context.performQuery(q); queryInterceptor.runWithQueriesBlocked(new UnitTestClosure() { public void execute() { // without OUTER join we will get fewer objects... assertEquals(2, objects.size()); Iterator<?> it = objects.iterator(); while (it.hasNext()) { Artist a = (Artist) it.next(); List<?> list = a.getPaintingArray(); assertNotNull(list); assertFalse(((ValueHolder) list).isFault()); assertTrue(list.size() > 0); Iterator<?> children = list.iterator(); while (children.hasNext()) { Painting p = (Painting) children.next(); assertEquals(PersistenceState.COMMITTED, p.getPersistenceState()); // make sure properties are not null.. assertNotNull(p.getPaintingTitle()); } } } }); } @Test public void testJointPrefetchToOne() throws Exception { createJointPrefetchDataSet1(); // query with to-many joint prefetches SelectQuery q = new SelectQuery(Painting.class); q.addOrdering("db:PAINTING_ID", SortOrder.ASCENDING); q.addPrefetch(Painting.TO_ARTIST.joint()); final List<?> objects = context.performQuery(q); queryInterceptor.runWithQueriesBlocked(new UnitTestClosure() { public void execute() { assertEquals(3, objects.size()); Iterator<?> it = objects.iterator(); while (it.hasNext()) { Painting p = (Painting) it.next(); Artist target = p.getToArtist(); assertNotNull(target); assertEquals(PersistenceState.COMMITTED, target.getPersistenceState()); } } }); } /** * Tests that joined entities can have non-standard type mappings. */ @Test public void testJointPrefetchDataTypes() throws Exception { // prepare... can't load from XML, as it doesn't yet support dates.. SQLTemplate artistSQL = new SQLTemplate( Artist.class, "insert into ARTIST (ARTIST_ID, ARTIST_NAME, DATE_OF_BIRTH) " + "values (33001, 'a1', #bind($date 'DATE'))"); artistSQL.setParams(Collections.singletonMap( "date", new Date(System.currentTimeMillis()))); SQLTemplate paintingSQL = new SQLTemplate( Painting.class, "INSERT INTO PAINTING (PAINTING_ID, PAINTING_TITLE, ARTIST_ID, ESTIMATED_PRICE) " + "VALUES (33001, 'p1', 33001, 1000)"); context.performNonSelectingQuery(artistSQL); context.performNonSelectingQuery(paintingSQL); // test SelectQuery q = new SelectQuery(Painting.class); q.addPrefetch(Painting.TO_ARTIST.joint()); ObjEntity artistE = context.getEntityResolver().getObjEntity("Artist"); ObjAttribute dateOfBirth = artistE.getAttribute("dateOfBirth"); assertEquals("java.util.Date", dateOfBirth.getType()); dateOfBirth.setType("java.sql.Date"); try { final List<?> objects = context.performQuery(q); queryInterceptor.runWithQueriesBlocked(new UnitTestClosure() { public void execute() { assertEquals(1, objects.size()); Iterator<?> it = objects.iterator(); while (it.hasNext()) { Painting p = (Painting) it.next(); Artist a = p.getToArtist(); assertNotNull(a); assertNotNull(a.getDateOfBirth()); assertTrue(a.getDateOfBirth().getClass().getName(), Date.class .isAssignableFrom(a.getDateOfBirth().getClass())); } } }); } finally { dateOfBirth.setType("java.util.Date"); } } @Test public void testJointPrefetchToMany() throws Exception { createJointPrefetchDataSet1(); // query with to-many joint prefetches SelectQuery q = new SelectQuery(Artist.class); q.addPrefetch(Artist.PAINTING_ARRAY.joint()); final List<?> objects = context.performQuery(q); queryInterceptor.runWithQueriesBlocked(new UnitTestClosure() { public void execute() { assertEquals(3, objects.size()); Iterator<?> it = objects.iterator(); while (it.hasNext()) { Artist a = (Artist) it.next(); List<?> list = a.getPaintingArray(); assertNotNull(list); assertFalse(((ValueHolder) list).isFault()); Iterator<?> children = list.iterator(); while (children.hasNext()) { Painting p = (Painting) children.next(); assertEquals(PersistenceState.COMMITTED, p.getPersistenceState()); // make sure properties are not null.. assertNotNull(p.getPaintingTitle()); } } } }); } @Test public void testJointPrefetchToManyNonConflictingQualifier() throws Exception { createJointPrefetchDataSet1(); // query with to-many joint prefetches and qualifier that doesn't match // prefetch.... Expression qualifier = Artist.ARTIST_NAME.eq("artist1"); SelectQuery q = new SelectQuery(Artist.class, qualifier); q.addPrefetch(Artist.PAINTING_ARRAY.joint()); final List<?> objects = context.performQuery(q); queryInterceptor.runWithQueriesBlocked(new UnitTestClosure() { public void execute() { assertEquals(1, objects.size()); Artist a = (Artist) objects.get(0); List<?> list = a.getPaintingArray(); assertNotNull(list); assertFalse(((ValueHolder) list).isFault()); assertEquals(2, list.size()); Iterator<?> children = list.iterator(); while (children.hasNext()) { Painting p = (Painting) children.next(); assertEquals(PersistenceState.COMMITTED, p.getPersistenceState()); // make sure properties are not null.. assertNotNull(p.getPaintingTitle()); } // assert no duplicates Set s = new HashSet(list); assertEquals(s.size(), list.size()); } }); } @Test public void testJointPrefetchMultiStep() throws Exception { createJointPrefetchDataSet2(); // query with to-many joint prefetches SelectQuery<Artist> q = new SelectQuery<>(Artist.class); q.addPrefetch(Artist.PAINTING_ARRAY.dot(Painting.TO_GALLERY).joint()); final DataContext context = this.context; // make sure phantomly prefetched objects are not deallocated context.getObjectStore().objectMap = new HashMap<Object, Persistent>(); // sanity check... DataObject g1 = (DataObject) context.getGraphManager().getNode( new ObjectId("Gallery", Gallery.GALLERY_ID_PK_COLUMN, 33001)); assertNull(g1); final List<?> objects = context.performQuery(q); queryInterceptor.runWithQueriesBlocked(new UnitTestClosure() { public void execute() { assertEquals(3, objects.size()); Iterator<?> it = objects.iterator(); while (it.hasNext()) { Artist a = (Artist) it.next(); ValueHolder list = (ValueHolder) a.getPaintingArray(); assertNotNull(list); // intermediate relationship is not fetched... assertTrue(list.isFault()); } // however both galleries must be in memory... DataObject g1 = (DataObject) context.getGraphManager().getNode( new ObjectId("Gallery", Gallery.GALLERY_ID_PK_COLUMN, 33001)); assertNotNull(g1); assertEquals(PersistenceState.COMMITTED, g1.getPersistenceState()); DataObject g2 = (DataObject) context.getGraphManager().getNode( new ObjectId("Gallery", Gallery.GALLERY_ID_PK_COLUMN, 33002)); assertNotNull(g2); assertEquals(PersistenceState.COMMITTED, g2.getPersistenceState()); } }); } }