diff --git a/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/sql/DBDictionary.java b/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/sql/DBDictionary.java index eb1d3b613..20e9a8451 100644 --- a/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/sql/DBDictionary.java +++ b/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/sql/DBDictionary.java @@ -355,6 +355,7 @@ public class DBDictionary protected Log log = null; protected boolean connected = false; protected boolean isJDBC3 = false; + protected boolean isJDBC4 = false; protected final Set reservedWordSet = new HashSet(); // reservedWordSet subset that CANNOT be used as valid column names // (i.e., without surrounding them with double-quotes) @@ -423,9 +424,11 @@ public class DBDictionary try { metaData = conn.getMetaData(); try { - // JDBC3-only method, so it might throw a + // JDBC3-only method, so it might throw an // AbstractMethodError - isJDBC3 = metaData.getJDBCMajorVersion() >= 3; + int JDBCMajorVersion = metaData.getJDBCMajorVersion(); + isJDBC3 = JDBCMajorVersion >= 3; + isJDBC4 = JDBCMajorVersion >= 4; } catch (Throwable t) { // ignore if not JDBC3 } @@ -5437,4 +5440,16 @@ public class DBDictionary } return conversionKey; } + + /** + * Return parameter marker for INSERT and UPDATE statements. + * Usually it is ? but some database-specific types might require customization. + * + * @param col column definition + * @param val value to be inserted/updated + * @return parameter marker + */ + public String getMarkerForInsertUpdate(Column col, Object val) { + return "?"; + } } diff --git a/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/sql/OracleDictionary.java b/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/sql/OracleDictionary.java index 4f4fb3ab8..1115e1854 100644 --- a/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/sql/OracleDictionary.java +++ b/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/sql/OracleDictionary.java @@ -19,6 +19,7 @@ package org.apache.openjpa.jdbc.sql; import java.io.InputStream; +import java.io.StringReader; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.security.AccessController; @@ -105,6 +106,11 @@ public class OracleDictionary */ public boolean useSetFormOfUseForUnicode = true; + /** + * Type constructor for XML column, used in INSERT and UPDATE statements. + */ + public String xmlTypeMarker = "XMLType(?)"; + // some oracle drivers have problems with select for update; warn the // first time locking is attempted private boolean _checkedUpdateBug = false; @@ -237,9 +243,9 @@ public class OracleDictionary timestampTypeName = "DATE"; // added oracle 9 supportsXMLColumn = false; } - // select of an xml column requires ".getStringVal()" - // suffix. eg. t0.xmlcol.getStringVal() - getStringVal = ".getStringVal()"; + // select of an xml column requires ".getStringVal()" (for values <= 4000 bytes only) + // or ".getClobVal()" suffix. eg. t0.xmlcol.getClobVal() + getStringVal = ".getClobVal()"; } else if (metadataClassName.startsWith("com.ddtek.") || url.indexOf("jdbc:datadirect:oracle:") != -1 || "Oracle".equals(driverName)) { @@ -249,6 +255,7 @@ public class OracleDictionary driverVendor = VENDOR_OTHER; } cacheDriverBehavior(driverVendor); + guessJDBCVersion(conn); } /** @@ -560,6 +567,16 @@ public class OracleDictionary public void setClobString(PreparedStatement stmnt, int idx, String val, Column col) throws SQLException { + if (col.isXML()) { + if (isJDBC4) { + // This JDBC 4 method handles values longer than 4000 bytes. + stmnt.setClob(idx, new StringReader(val), val.length()); + } else { + // This method is limited to 4000 bytes. + setCharacterStream(stmnt, idx, new StringReader(val), val.length(), col); + } + return; + } if (!useSetStringForClobs && val.length() == 0) stmnt.setClob(idx, getEmptyClob()); else { @@ -1310,4 +1327,36 @@ public class OracleDictionary row.setBlob(col, getEmptyBlob()); } } + + /** + * Oracle requires special handling of XML column. + * Unless the value length is less or equal to 4000 bytes, + * the parameter marker must be decorated with type constructor. + */ + @Override + public String getMarkerForInsertUpdate(Column col, Object val) { + if (col.isXML() && val != RowImpl.NULL) { + return xmlTypeMarker; + } + return super.getMarkerForInsertUpdate(col, val); + } + + /** + * Oracle drivers, at least in versions 10.2.0.4 and 11.2.0.1, incorrectly return a driver major version from + * {@link DatabaseMetaData#getJDBCMajorVersion()}. + */ + protected void guessJDBCVersion(Connection conn) { + if (_driverBehavior != BEHAVE_ORACLE) { + return; + } + isJDBC4 = true; + try { + conn.getClientInfo(); // Try to call a JDBC 4 method. + } catch (SQLException e) { + // OK, we are on JDBC 4. + } catch (Throwable t) { + // Most likely an AbstractMethodError from JDBC 3 driver. + isJDBC4 = false; + } + } } diff --git a/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/sql/RowImpl.java b/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/sql/RowImpl.java index d712a203f..e3d9a006c 100644 --- a/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/sql/RowImpl.java +++ b/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/sql/RowImpl.java @@ -758,8 +758,10 @@ public class RowImpl buf.append(dict.getColumnDBName(_cols[i])); if (_types[i] == RAW) buf.append(" = ").append(_vals[i]); - else - buf.append(" = ?"); + else { + buf.append(" = "); + buf.append(dict.getMarkerForInsertUpdate(_cols[i], _vals[i])); + } hasVal = true; } @@ -789,7 +791,7 @@ public class RowImpl if (_types[i] == RAW) vals.append(_vals[i]); else - vals.append("?"); + vals.append(dict.getMarkerForInsertUpdate(_cols[i], _vals[i])); hasVal = true; } diff --git a/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/xmlmapping/query/TestXMLCustomerOrder.java b/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/xmlmapping/query/TestXMLCustomerOrder.java index 577181799..ad5848e8d 100644 --- a/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/xmlmapping/query/TestXMLCustomerOrder.java +++ b/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/xmlmapping/query/TestXMLCustomerOrder.java @@ -26,6 +26,7 @@ import javax.persistence.Query; import junit.textui.TestRunner; +import org.apache.commons.lang.StringUtils; import org.apache.openjpa.jdbc.conf.JDBCConfiguration; import org.apache.openjpa.jdbc.sql.DBDictionary; import org.apache.openjpa.persistence.OpenJPAEntityManagerFactorySPI; @@ -64,9 +65,13 @@ public class TestXMLCustomerOrder private static final double ORDER_2_AMOUNT = 1000; private static final boolean ORDER_2_DELIVERED = false; + private static boolean firstTestExecuted; + public void setUp() { + Object clearOrDropTables = (firstTestExecuted) ? CLEAR_TABLES : DROP_TABLES; + firstTestExecuted = true; setUp(Customer.class, Customer.CustomerKey.class, Order.class, - EAddress.class, DROP_TABLES); // test create table DDL for XML column + EAddress.class, clearOrDropTables); // test create table DDL for XML column but only once to save time. // skip test if dictionary has no support for XML column type setTestsDisabled(!dictionarySupportsXMLColumn()); @@ -347,10 +352,11 @@ public class TestXMLCustomerOrder private USAAddress createUSAAddress(String name) { USAAddress address = new ObjectFactory().createUSAAddress(); address.setName(name); - address.getStreet().add("12500 Monterey"); + // Use a 4000-byte value so the entire XML string is longer than 4000 bytes - ensure Oracle handles this. + address.getStreet().add(StringUtils.repeat("12500 Mont", 400)); address.setCity("San Jose"); address.setState("CA"); - address.setZIP(new Integer("95141")); + address.setZIP(95141); return address; } diff --git a/openjpa-project/src/doc/manual/ref_guide_dbsetup.xml b/openjpa-project/src/doc/manual/ref_guide_dbsetup.xml index eaf74633b..c53d9ab0e 100644 --- a/openjpa-project/src/doc/manual/ref_guide_dbsetup.xml +++ b/openjpa-project/src/doc/manual/ref_guide_dbsetup.xml @@ -1490,8 +1490,8 @@ to "FOR UPDATE". GetStringVal: A special function to return the value of an XML column in a select statement. For example, Oracle uses -".getStringVal()", as in, -"select t0.xmlcol.getStringVal() from xmltab t0". +".getClobVal()", as in +"SELECT t0.xmlcol.getClobVal() FROM xmltab t0". Defaults to the empty string. @@ -1505,14 +1505,6 @@ Defaults to the empty string. InClauseLimit - - - JDBC - - - GetStringVal - - InClauseLimit: The maximum number of elements in an IN clause. OpenJPA works around cases where the limit is exceeded. Defaults to -1 meaning diff --git a/openjpa-project/src/doc/manual/ref_guide_mapping.xml b/openjpa-project/src/doc/manual/ref_guide_mapping.xml index 2e8729885..8b35724b3 100644 --- a/openjpa-project/src/doc/manual/ref_guide_mapping.xml +++ b/openjpa-project/src/doc/manual/ref_guide_mapping.xml @@ -2633,6 +2633,10 @@ SQL Server 2005 +See for possible database-specific +restrictions. + + Annotate the entity property using the XMLValueHandler strategy: diff --git a/openjpa-project/src/doc/manual/supported_databases.xml b/openjpa-project/src/doc/manual/supported_databases.xml index 991a75383..aaaa0a246 100644 --- a/openjpa-project/src/doc/manual/supported_databases.xml +++ b/openjpa-project/src/doc/manual/supported_databases.xml @@ -1206,6 +1206,14 @@ openjpa.ConnectionRetainMode: always + + +Mapping persistent attributes to XML columns requires +a JDBC 4 compliant driver if XML strings are longer than 4000 bytes, as counted in database. +Otherwise an ORA-01461: can bind a LONG value only for insert into a LONG column +error may result. + +