/***************************************************************** * 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.query; import org.apache.cayenne.DataRow; import org.apache.cayenne.ProcedureResult; import org.apache.cayenne.access.DataContext; import org.apache.cayenne.access.jdbc.ColumnDescriptor; import org.apache.cayenne.di.Inject; import org.apache.cayenne.log.JdbcEventLogger; import org.apache.cayenne.testdo.testmap.Artist; import org.apache.cayenne.testdo.testmap.Painting; import org.apache.cayenne.tx.BaseTransaction; import org.apache.cayenne.tx.ExternalTransaction; import org.apache.cayenne.unit.UnitDbAdapter; 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.Test; import java.math.BigDecimal; import java.sql.Types; import java.util.List; import java.util.Map; import static org.junit.Assert.*; @UseServerRuntime(CayenneProjects.TESTMAP_PROJECT) public class ProcedureCallIT extends ServerCase { public static final String UPDATE_STORED_PROCEDURE = "cayenne_tst_upd_proc"; public static final String UPDATE_STORED_PROCEDURE_NOPARAM = "cayenne_tst_upd_proc2"; public static final String SELECT_STORED_PROCEDURE = "cayenne_tst_select_proc"; public static final String OUT_STORED_PROCEDURE = "cayenne_tst_out_proc"; @Inject private DataContext context; @Inject private UnitDbAdapter accessStackAdapter; @Inject private JdbcEventLogger jdbcEventLogger; @Test public void testUpdate() throws Exception { if (!accessStackAdapter.supportsStoredProcedures()) { return; } // create an artist with painting in the database createArtist(1000.0); runProcedureSelect(ProcedureCall.query(UPDATE_STORED_PROCEDURE).param("paintingPrice", 3000)); // check that price have doubled SelectQuery select = new SelectQuery<>(Artist.class); select.addPrefetch("paintingArray"); List artists = context.performQuery(select); assertEquals(1, artists.size()); Artist a = (Artist) artists.get(0); Painting p = a.getPaintingArray().get(0); assertEquals(2000, p.getEstimatedPrice().intValue()); } @Test public void testUpdateNoParam() throws Exception { if (!accessStackAdapter.supportsStoredProcedures()) { return; } // create an artist with painting in the database createArtist(1000.0); runProcedureSelect(ProcedureCall.query(UPDATE_STORED_PROCEDURE_NOPARAM)); // check that price have doubled SelectQuery select = new SelectQuery<>(Artist.class); select.addPrefetch("paintingArray"); List<?> artists = context.performQuery(select); assertEquals(1, artists.size()); Artist a = (Artist) artists.get(0); Painting p = a.getPaintingArray().get(0); assertEquals(2000, p.getEstimatedPrice().intValue()); } @Test public void testSelect() throws Exception { if (!accessStackAdapter.supportsStoredProcedures()) { return; } // create an artist with painting in the database createArtist(1000.0); List<?> artists = runProcedureSelect( ProcedureCall.query(SELECT_STORED_PROCEDURE) .param("aName", "An Artist") .param("paintingPrice", 3000) ).firstList(); // check the results assertNotNull("Null result from StoredProcedure.", artists); assertEquals(1, artists.size()); DataRow artistRow = (DataRow) artists.get(0); Artist a = context.objectFromDataRow(Artist.class, uppercaseConverter(artistRow)); Painting p = a.getPaintingArray().get(0); // invalidate painting, it may have been updated in the proc context.invalidateObjects(p); assertEquals(2000, p.getEstimatedPrice().intValue()); } @Test public void testFetchLimit() throws Exception { if (!accessStackAdapter.supportsStoredProcedures()) { return; } // create an artist with painting in the database createArtist(1000.0); createArtist(2000.0); createArtist(3000.0); List<?> artists = runProcedureSelect( ProcedureCall.query(SELECT_STORED_PROCEDURE) .param("aName", "An Artist") .param("paintingPrice", 3000) .limit(2) ).firstList(); assertEquals(2, artists.size()); } @Test public void testFetchOffset() throws Exception { if (!accessStackAdapter.supportsStoredProcedures()) { return; } // create an artist with painting in the database createArtist(1000.0); createArtist(2000.0); createArtist(3000.0); List<?> artists = runProcedureSelect( ProcedureCall.query(SELECT_STORED_PROCEDURE) .param("aName", "An Artist") .param("paintingPrice", 3000) .offset(2) ).firstList(); assertEquals(1, artists.size()); } @Test public void testColumnNameCapitalization() throws Exception { if (!accessStackAdapter.supportsStoredProcedures()) { return; } // create an artist with painting in the database createArtist(1000.0); List<DataRow> artists = runProcedureSelect( ProcedureCall.query(SELECT_STORED_PROCEDURE) .param("aName", "An Artist") .capsStrategy(CapsStrategy.LOWER) ).firstList(); List<DataRow> artists1 = runProcedureSelect(ProcedureCall.query(SELECT_STORED_PROCEDURE) .param("aName", "An Artist") .capsStrategy(CapsStrategy.UPPER) ).firstList(); assertTrue(artists.get(0).containsKey("date_of_birth")); assertFalse(artists.get(0).containsKey("DATE_OF_BIRTH")); assertFalse(artists1.get(0).containsKey("date_of_birth")); assertTrue(artists1.get(0).containsKey("DATE_OF_BIRTH")); } @Test public void testOutParams() throws Exception { if (!accessStackAdapter.supportsStoredProcedures()) { return; } ProcedureResult result = runProcedureSelect( ProcedureCall.query(OUT_STORED_PROCEDURE) .param("in_param", 20) ); Number price = (Number) result.getOutParam("out_param"); assertEquals(40, price.intValue()); } @Test public void testSelectDataObject() throws Exception { if (!accessStackAdapter.supportsStoredProcedures()) { return; } if (!accessStackAdapter.canMakeObjectsOutOfProcedures()) { return; } // create an artist with painting in the database createArtist(1101.01); List<Artist> artists = runProcedureSelect(ProcedureCall.query(SELECT_STORED_PROCEDURE, Artist.class) .param("aName", "An Artist") ).firstList(); // check the results assertNotNull("Null result from StoredProcedure.", artists); assertEquals(1, artists.size()); Artist a = (Artist) artists.get(0); Painting p = a.getPaintingArray().get(0); // invalidate painting, it may have been updated in the proc context.invalidateObjects(p); assertEquals(1101.01, p.getEstimatedPrice().doubleValue(), 0.02); } @Test public void testSelectWithRowDescriptor() throws Exception { if (!accessStackAdapter.supportsStoredProcedures()) { return; } // create an artist with painting in the database createArtist(1000.0); // TESTING THIS *** // A.ARTIST_ID, A.DATE_OF_BIRTH, A.ARTIST_NAME ColumnDescriptor[] columns = new ColumnDescriptor[3]; // read ID as Long, and everything else as default types columns[0] = new ColumnDescriptor("ARTIST_ID", Types.BIGINT); columns[1] = new ColumnDescriptor("ARTIST_NAME", Types.CHAR); columns[2] = new ColumnDescriptor("DATE_OF_BIRTH", Types.DATE); List<?> rows = runProcedureSelect(ProcedureCall.dataRowQuery(SELECT_STORED_PROCEDURE) .param("aName", "An Artist") .param("paintingPrice", 3000) .resultDescriptor(columns) ).firstList(); // check the results assertNotNull("Null result from StoredProcedure.", rows); assertEquals(1, rows.size()); DataRow artistRow = (DataRow) rows.get(0); assertEquals(3, artistRow.size()); artistRow = uppercaseConverter(artistRow); Object id = artistRow.get("ARTIST_ID"); assertNotNull(id); assertTrue("Expected Long, got: " + id.getClass().getName(), id instanceof Long); } protected <T> ProcedureResult<T> runProcedureSelect(ProcedureCall<T> q) throws Exception { // Sybase blows whenever a transaction wraps a SP, so turn off // transactions // TODO: it is quite the opposite with PostgreSQL. If an SP returns an // open refcursor, it actually expects a TX in progress, so while we // don't have refcursor unit tests, this is something to keep in mind // e.g. // http://stackoverflow.com/questions/16921942/porting-apache-cayenne-from-oracle-to-postgresql BaseTransaction t = new ExternalTransaction(jdbcEventLogger); BaseTransaction.bindThreadTransaction(t); try { return q.call(context); } finally { BaseTransaction.bindThreadTransaction(null); t.commit(); } } protected void createArtist(double paintingPrice) { Artist a = context.newObject(Artist.class); a.setArtistName("An Artist"); Painting p = context.newObject(Painting.class); p.setPaintingTitle("A Painting"); // converting double to string prevents rounding weirdness... p.setEstimatedPrice(new BigDecimal("" + paintingPrice)); a.addToPaintingArray(p); context.commitChanges(); } /** * An ugly hack - converting row keys to uppercase ... Tracked via CAY-148. */ protected DataRow uppercaseConverter(DataRow row) { DataRow converted = new DataRow(row.size()); for (Map.Entry<String, Object> entry : row.entrySet()) { converted.put(entry.getKey().toString().toUpperCase(), entry.getValue()); } return converted; } }