Tuesday, March 4, 2008

Oracle 10g : How to pass ARRAYS of records from Java/Tomcat to Oracle

Environment : JDK 1.5, Tomcat 5.5, Oracle 10gR2.

Let's suppose that you have a web app where you get some records from the user interface and from your DAO, you are trying to pass the records as Oracle ARRAYS to database. Here is a step by step example.

Yes, it's a nightmare as you have to take care of some steps. But, once you understand what to do and what are the issues, it's pretty easy.

Use Case : Let us suppose that we have a java bean Employee and we are trying to send an array of employee records at a time to database.

/*Step 1 : Create a object type in the database*/
/*
CREATE OR REPLACE TYPE "EMP_TYPE" is object(
emp_id Varchar2(500),
emp_name varchar2(500));

*/
/*
Step 2 : Create a type EMP_TYPE_TABLE

CREATE OR REPLACE TYPE "EMP_TYPE_TAB";
*/

/*Step 3 : Create a Java bean which maps the attributes of the above object type in Step 2.*/

import java.io.Serializable;
import java.sql.SQLData;
import java.sql.SQLException;
import java.sql.SQLInput;
import java.sql.SQLOutput;

public class Employee implements SQLData, Serializable{
static final long serialVersionUID = 4070409649129120458L;
public Employee(){}

// constructor that takes parameters
// getters and setters for emp_id, emp_name
// You have to implement readSQL() and writeSQL() methods, as shown below.
// This is where you are mapping the Employee table's columns to the Employee
//java bean.

public void readSQL(SQLInput stream, String typeName) throws SQLException {
this.emp_id = stream.readString();
this.emp_name = stream.readString();
}
public void writeSQL(SQLOutput stream) throws SQLException {
stream.writeString(emp_id);
stream.writeString(emp_name);
}
}

//EmpDAO class gets a connection to the database and passes the data.

public class EmpDAO{
java.sql.Connection conn;
java.sql.Connection dconn;
/*
Step 1 : Get database connection
This is a very important step. To pass your records of data as Arrays, you need to get a oracle.jdbc.driver.T4CConnection and then use ArrayDescriptor's. So, how do you get a T4CConnection ?


To get T4CConnection from java.sql.Connection, you need to cast like this :
t4cConn = ((DelegatingConnection)conn).getInnermostDelegate();

If you are working on tomcat, you have two options to get a DataSource in your context.xml.
a) By using apache commons-dbcp
OR
b) by directly using javax.sql.DataSource.
Let's see how to get the T4CConnection in both these cases.
*/

public void sendRecordsToDB(){

//Use Case (a) : if you configured apache commons-dbcp
BasicDataSource ds = (BasicDataSource)ctx.lookup(jndiName);
ds.setAccessToUnderlyingConnectionAllowed(true);
conn = ds.getConnection();
dconn = ((DelegatingConnection)conn).getInnermostDelegate();

//Use Case (b) : if you are directly using javax.sql.DataSource

BasicDataSource bds = new BasicDataSource();
bds.setDriverClassName("");
bds.setUsername("");
bds.setPassword("");
bds.setUrl("jdbc:oracle:thin:@");
bds.setAccessToUnderlyingConnectionAllowed(true);
conn = bds.getConnection();
dconn = ((DelegatingConnection)conn).getInnermostDelegate();

/*So, using either of the above approaches we got dconn, which is an instance of T4CConnection.*/

/* Now let's build an array list of employees.
*/
final List listOfEmployees = new LinkedList();

Employee e1 = new Employee();
e.setEmpId(1);
e.setEmpName("Ravi");

listOfEmployees.add(e1);

Employee e2 = new Employee();
e.setEmpId(2);
e.setEmpName("Vedala");

listOfEmployees.add(e2);

// Now, create an array descriptor

ArrayDescriptor descriptor = ArrayDescriptor.createDescriptor( "EMP_TYPE_TAB", dconn );

ARRAY array_to_pass = new ARRAY( descriptor, dconn, (Object[]) listOfEmployees.toArray());
ps = (OracleCallableStatement)dconn.prepareCall("begin insert_employees(:1); end;");
ps.setARRAY( 1, array_to_pass );
ps.execute();
conn.commit();
}

/*
- See how simple and beautiful is the procedure.
- Using the TABLE() function, you can treat the whole array as a table as EMP_TYPE_TAB is a nested table.
*/

PROCEDURE insert_employees(p_emparray in EMP_TYPE_TAB) AS
BEGIN
/* INSERT ARRAY OF RECORDS IN TO THE EMP TABLE*/
INSERT INTO scd_company_staging
(emp_id,emp_name)
SELECT * FROM TABLE(p_empparray);
END insert_employees;

The nightmare exception for Java/Oracle developers :-)

java.lang.ClassCastException: oracle.jdbc.driver.T4CConnection cannot be cast to oracle.jdbc.OracleConnection
at oracle.sql.ArrayDescriptor.createDescriptor(ArrayDescriptor.java:149)
at oracle.sql.ArrayDescriptor.createDescriptor(ArrayDescriptor.java:115)
...........

Solution :
a)You will see the above exception, if you have ojdbc14.jar in your war file. You would be having ojdbc14.jar on your classpath for compiling your java classes. Use it only for compilation. Don't include it in the build to Tomcat. ie., the war file of your web app should NOT have ojdbc14.jar in it.

b) Make sure that the Oracle thin driver (eg : ojdbc14.jar) is in tomcat's common\lib.

Long live "Tom Kyte".


Good Luck !!
r-a-v-i

14 comments:

Mukesh Khandelwal said...

Shouldn't
SELECT * FROM TABLE(p_comparray);
be
SELECT * FROM TABLE(p_emparray);

in the procedure insert_employees

r-a-v-i said...

Thanks for the correction. You are correct. It should be p_emparray.

Dino said...

Excellent instructions. Thank you! I got this working very quickly thanks to this article.

Unknown said...

hi,

i am not able to import oracle.jdbc.driver.T4CConnection into my class even though it was there in the class path.

i am getting error like class is not visible.

i am using ojdbc14-10.2.0.2.jar file.

oracle.jdbc.driver.T4CConnection con = ((DelegatingConnection) getConnection())
.getInnermostDelegate();

help me.

r-a-v-i said...

If you are using a Java IDE like Eclipse, check your build path. Looks like the thin driver is not in your IDE's classpath, even though it is in your command line's classpath. This is just a guess, based on what the information you provided.

Unknown said...

hi,

i forgot to mention that i am using Eclipse.

jar file is there in the build path.

Tomcat: server.xml entry

Resource name="jdbc/abc" auth="Container" type="javax.sql.DataSource" maxActive="50" maxIdle="20" maxWait="10000"
username="rconstruct_system" password="rconstruct_system" driverClassName="oracle.jdbc.driver.OracleDriver" url="jdbc:oracle:thin:@localhost:1521:MFARDB" removeAbandoned="true" logAbandoned="true"
accessToUnderlyingConnectionAllowed=true

Code snippet in java:

Connection con = getConnection();
con = ((DelegatingConnection) con).getInnermostDelegate();

Exception:

java.lang.ClassCastException: org.apache.tomcat.dbcp.dbcp.PoolingDataSource$Pool
GuardConnectionWrapper cannot be cast to org.apache.commons.dbcp.DelegatingConne
ction


thanks in advance.

r-a-v-i said...

Are you using BasicDataSource ?

BasicDataSource ds = (BasicDataSource)ctx.lookup(jndiName);
ds.setAccessToUnderlyingConnectionAllowed(true);
conn = ds.getConnection();
dconn = ((DelegatingConnection)conn).getInnermostDelegate();

Can you provide getConnection() method's source code as well.

Thanks,
r-a-v-u

Unknown said...

hi ravi,

thanks for your reply,

that is the problem with incompatable jars in common/lib and web-inf/lib.

so i copied naming-dbcp.jar to web-inf/lib then it is working.

now i am getting new exception like 'java.sql.SQLException: Inconsistent java and sql object types'.

code:

String[] regions = to.getRegionsIds();
CentralRegionBudgetDetRow[] array = new CentralRegionBudgetDetRow[regions.length];
for (int i = 0; i < regions.length; i++) {
CentralRegionBudgetDetRow eachRow = new CentralRegionBudgetDetRow();
eachRow.setForeignkey(to.getPrimaryKey());
eachRow.setRegionid(regions[i]);
eachRow.setAmount(to.getBudgetAmount()[i]);
array[i] = eachRow;
eachRow = null;
}
cs = (OracleCallableStatement) con.prepareCall(objQL.getQuery(
"query", "MM4"));
StructDescriptor descriptor = StructDescriptor.createDescriptor(
"TD_CENTRAL_REGIONAL_BUDGET", con);
oracle.sql.STRUCT array_to_pass = new oracle.sql.STRUCT(descriptor,
con, array);


CentralRegionBudgetDetRow.java:

import java.io.Serializable;
import java.sql.SQLData;
import java.sql.SQLException;
import java.sql.SQLInput;
import java.sql.SQLOutput;

public class CentralRegionBudgetDetRow implements SQLData, Serializable {

private static final long serialVersionUID = 1L;
private String foreignkey;
private String regionid;
private Double amount;
private String sql_type = "TD_CENTRAL_REGIONAL_BUDGET";

public CentralRegionBudgetDetRow() {
}

// constructor that takes parameters
// getters and setters for emp_id, emp_name
// You have to implement readSQL() and writeSQL() methods, as shown below.
// This is where you are mapping the Employee table's columns to the
// Employee
// java bean.

public void readSQL(SQLInput stream, String typeName) throws SQLException {
this.foreignkey = stream.readString();
this.regionid = stream.readString();
this.amount = stream.readDouble();
}

public void writeSQL(SQLOutput stream) throws SQLException {
stream.writeString(getForeignKey());
stream.writeString(getRegionId());
stream.writeDouble(getAmount());
}

public String getForeignKey() {
return foreignkey;
}

public void setForeignkey(String foreignkey) {
this.foreignkey = foreignkey;
}

public String getRegionId() {
return regionid;
}

public void setRegionid(String regionid) {
this.regionid = regionid;
}

public Double getAmount() {
return amount;
}

public void setAmount(Double amount) {
this.amount = amount;
}

@Override
public String getSQLTypeName() throws SQLException {
// TODO Auto-generated method stub
return sql_type;
}
}

Database:

CREATE OR REPLACE TYPE "TR_CENTRAL_REGIONAL_BUDGET" AS OBJECT
(
foreignKey VARCHAR2(20),
regionId VARCHAR2(20),
amount NUMBER(18,2)
)

CREATE OR REPLACE TYPE "TD_CENTRAL_REGIONAL_BUDGET" IS TABLE OF TR_CENTRAL_REGIONAL_BUDGET

thanks in advance.

Brahma.

r-a-v-i said...

Not sure why you are using a StructDescriptor instead of an ArrayDescriptor. Also, where are you creating the SQL ARRAY and setting it on the CS ?

Look at the example :

// Create a Array Descriptor
ArrayDescriptor descriptor = ArrayDescriptor.createDescriptor( "EMP_TYPE_TAB", dconn );

// Create a SQL ARRAY for the descriptor
ARRAY array_to_pass = new ARRAY( descriptor, dconn, (Object[]) listOfEmployees.toArray());

// Create CS and set the array on it.
ps = (OracleCallableStatement)dconn.prepareCall("begin insert_employees(:1); end;");
ps.setARRAY( 1, array_to_pass );
ps.execute();
conn.commit();

You are giving me bits and pieces of information which is of no help for me. Next time, send me all the code (if you can) and the full stack trace of the exception.

Good Luck buddy,

r-a-v-i

Anonymous said...

Amiable dispatch and this fill someone in on helped me alot in my college assignement. Thanks you as your information.

Shaj said...

Hi,

I am passing an array of records from java to Oracle exacly same way. I am facing a problem when one of the value writing back is null.

public void writeSQL( SQLOutput stream ) throws SQLException {
...
stream.writeDouble( numericValue );
...
}

Here some cases numericValue can be Null and in that instance, its throwing a NullPointer Exception.
Is there a way to Write an SQLNULL using SQLData?

Thanks
SHaj

Sujeeth Kumar said...

Ravi,

Is it not possible to write array ibject without using ARRAY class of Oracle JDBC API. Use of this API tight cupled with Oralc eonly. Cant we operate on java.sql.Array interface ?

Thanks,
Sujeeth

r-a-v-i said...

I haven't tried, but I think you can do it.

Kshitija said...

very useful article. well it is working for me but I am not able to get connection from existing basicdatasource bean defined in dispatcher-servlet ,I get the connection but after using getinnermostdeligate my connection becomes null. So I have to explicitly define url,user name ,password etc in order to make connection please kindly help.I am using Spring.