/*****************************************************************
* 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;
}
}