This is my first time posting a question in stackoverflow,
I need to write a java stored procedure that creates an excel file and returns a blob containing the file data in bytes.
My pl/sql function is in the following form
function test_create_excel(i_username IN varchar2) return BLOB
AS LANGUAGE JAVA NAME 'NTO.Excel.ExcelFunctions.PushToExcel( java.lang.String ) return java.sql.Blob';
my Java method is as follows
public static java.sql.Blob TestPushToExcel(String username) throws IOException, SQLException{
//create excel file, read content to byte array and set to a blob
}
My problem is that I cannot find any way to create an instance of java.sql.Blob so that i can use the blob.setBinaryStream(..) method to write the file data byte array.
I tried to use the SerialBlob implementation but it results in the following oracle error
ORA-00932: inconsistent datatypes: expected a return value that is an
instance of a user defined Java class convertible to an Oracle type
got an object that could not be converted
has anyone come across this issue and if so can you share on how you got through it.
Thank you in Advance.
EDIT
JAVA
public static oracle.sql.BLOB getBlob(byte[] data) throws SQLException, IOException{
oracle.jdbc.OracleConnection conn = (oracle.jdbc.OracleConnection)new OracleDriver().defaultConnection();
oracle.sql.BLOB retBlob = oracle.sql.BLOB.createTemporary(conn, true, oracle.sql.BLOB.DURATION_SESSION);
java.io.OutputStream outStr = retBlob.setBinaryStream(0);
outStr.write(data);
outStr.flush();
return retBlob;
}
public static ExcelFileStore PushToExcel(String userId) throws IOException, SQLException{
ExcelFileStore fileStore = new ExcelFileStore();
fileStore.NU_USERID = userId;
fileStore.CreatedTime = new java.sql.Date(new Date().getTime());
fileStore.Last_Updated = new java.sql.Date(new Date().getTime());
fileStore.FileSize = fileData.length;
fileStore.FileData = getBlob(fileData);
return fileStore;
}
PL/SQL
function test_create_excel(i_username IN varchar2) return EXCELFILESTORE AS LANGUAGE JAVA NAME 'NTO.Excel.ExcelFunctions.PushToExcel( java.lang.String, ) return OracleObjects.ExcelFileStore';
OracleObject.ExcelfileStore is a class that implements java.sql.SqlData and EXCELFILESTORE is a UDT in oracle.
I loaded the reference jars and jar created for my code using 'sys.dbms_java.loadjava'
I hope you understand my question, as i'm quite new to pl/sql programming
I was wrong. It can be done. Took me a while to get it to work, but, finally, here is a working example:
Java class
import oracle.jdbc.driver.*;
public class TestBlob {
public static oracle.sql.BLOB getBlob(String username) throws Exception {
oracle.jdbc.OracleConnection conn =
(oracle.jdbc.OracleConnection)new OracleDriver().defaultConnection();
oracle.sql.BLOB retBlob =
oracle.sql.BLOB.createTemporary(conn,
true,
oracle.sql.BLOB.DURATION_SESSION);
java.io.OutputStream outStr = retBlob.setBinaryStream(0);
outStr.write(username.getBytes());
outStr.flush();
return retBlob;
}
}
As you can see, I have used the oracle.sql.BLOB for the result. I create it with the static createTemporary method of the BLOB class, specifying that it should be created for the duration of the session (oracle.sql.BLOB.DURATION_SESSION parameter).
Then, I obtain the OutputStream and write the data. Flush was needed.
Database side
create or replace FUNCTION getBlobWrp (username IN VARCHAR2) RETURN BLOB
AS LANGUAGE JAVA NAME
'TestBlob.getBlob(java.lang.String) return oracle.sql.BLOB';
Test:
DECLARE
l_blob BLOB;
BEGIN
l_blob := getBlobWrp('test');
dbms_output.put_line(UTL_RAW.CAST_TO_VARCHAR2(l_blob));
END;
Output:
test
(previous answer)
I think you should have an IN OUT BLOB parameter in your test_create_excel function (change it to a procedure), and operate on that parameter inside your Java stored method. I saw that approach once.
Before calling the test_create_excel, you should create a BLOB object:
DECLARE
l_blob BLOB;
BEGIN
DBMS_LOB.createtemporary(l_blob, TRUE);
test_create_excel('username', l_blob);
END;
Edit
I don't think what you're trying to do is possible. However, you could wrap the above code in another function. It's a bit messy, but then you'll have a function which returns the blob:
CREATE OR REPLACE FUNCTION get_excel_blob(p_username VARCHAR2) RETURN BLOB
AS
l_blob BLOB;
BEGIN
DBMS_LOB.createtemporary(l_blob, TRUE);
test_create_excel(p_username, l_blob);
RETURN l_blob;
END;
Related
I need to get single the GridFS file using Java driver 3.7+.
I have two collections with file in a database: photo.files and photo.chunks.
The photo.chunks collection contains the binary file like:
The photo.files collection contains the metadata of the document.
To find document using simple database I wrote:
Document doc = collection_messages.find(eq("flag", true)).first();
String messageText = (String) Objects.requireNonNull(doc).get("message");
I tried to find file and wrote in same way as with an example above, according to my collections on screens:
MongoDatabase database_photos = mongoClient.getDatabase("database_photos");
GridFSBucket photos_fs = GridFSBuckets.create(database_photos,
"photos");
...
...
GridFSFindIterable gridFSFile = photos_fs.find(eq("_id", new ObjectId()));
String file = Objects.requireNonNull(gridFSFile.first()).getMD5();
And like:
GridFSFindIterable gridFSFile = photos_fs.find(eq("_id", new ObjectId()));
String file = Objects.requireNonNull(gridFSFile.first()).getFilename();
But I get an error:
java.lang.NullPointerException
at java.util.Objects.requireNonNull(Objects.java:203)
at project.Bot.onUpdateReceived(Bot.java:832)
at java.util.ArrayList.forEach(ArrayList.java:1249)
Also I checked docs of 3.7 driver, but this example shows how to find several files, but I need single:
gridFSBucket.find().forEach(
new Block<GridFSFile>() {
public void apply(final GridFSFile gridFSFile) {
System.out.println(gridFSFile.getFilename());
}
});
Can someone show me an example how to realize it properly?
I mean getting data, e.g. in chunks collection by Object_id and md5 field also by Object_id in metadata collection.
Thanks in advance.
To find and use specific files:
photos_fs.find(eq("_id", objectId)).forEach(
(Block<GridFSFile>) gridFSFile -> {
// to do something
});
or as alternative, I can find specific field of the file.
It can be done firstly by creating objectId of the first file, then pass it to GridFSFindIterable object to get particular field and value from database and get finally file to convert into String.
MongoDatabase database_photos =
mongoClient.getDatabase("database_photos");
GridFSBucket photos_fs = GridFSBuckets.create(database_photos,
"photos");
...
...
ObjectId objectId = Objects.requireNonNull(photos_fs.find().first()).getObjectId();
GridFSFindIterable gridFSFindIterable = photos_fs.find(eq("_id", objectId));
GridFSFile gridFSFile = Objects.requireNonNull(gridFSFindIterable.first());
String file = Objects.requireNonNull(gridFSFile).getMD5();
But it checks files from photo.files not from photo.chunkscollection.
And I'm not sure that this way is code-safe, because of debug info, but it works despite the warning:
Inconvertible types; cannot cast 'com.mongodb.client.gridfs.model.GridFSFile' to 'com.mongodb.client.gridfs.GridFSFindIterableImpl'
I have tried to use excel as database for which I used the fillo dependency.
I have tried with a single parameter which is working fine. But when I try with multiple arguments it pops the following error :
" The method getField(String) in the type Recordset is not applicable
for the arguments (String[])", error is on "multi_data".
Here is the code:
public static void read_by_excel(String query, String field, String ... multi_data) throws FilloException
{
ArrayList<Object> excel_data = new ArrayList<Object>();
Fillo fillo = new Fillo();
Connection connection = fillo.getConnection("C:\\Users\\Vishrut\\Desktop\\read_openxl.xlsx");
String strQuery = query;
Recordset rs = connection.executeQuery(strQuery);
while (rs.next())
{
excel_data.add(rs.getField(field)+rs.getField(multi_data));
}
for (Object data: excel_data)
{
System.out.println(data);
}
rs.close();
connection.close();
}
Here how it is called. It can take multiple parameters after "department".
function_class.read_by_excel("select * from Sheet1 where employee_id = 60546", "department","salary");
I think the error message is clear, getField(String) method does not support being passed an array.
From your code, the multi_data parameter is an array
String ... multi_data
then you pass it to the getField method
rs.getField(multi_data)
Here is how I would do it : change the method signature
public static void read_by_excel(Integer emplyoee_id, String ...fields){
//...
}
Then replace
rs.getField(field) + rs.getField(multi_data)
by
Stream.of(fields).map(f -> rs.getField(f)).collect(Collectors.joining());
Note that I am assuming that getField returns a String (no public API doc for fillo so could not verify). Note sure you really want to do rs.getField(field) + rs.getField(multi_data) without a separator between the values. Consider using joining(",") or anything else.
I am trying to pass in an ARRAY of BLOBs, but I am getting errors.
uploadFiles = new SimpleJdbcCall(dataSource).withCatalogName("FILES_PKG")
.withFunctionName("insertFiles").withReturnValue()
.declareParameters(new SqlParameter("p_userId", Types.NUMERIC),
new SqlParameter("p_data", Types.ARRAY, "BLOB_ARRAY"),
new SqlOutParameter("v_groupId", Types.NUMERIC));
uploadFiles.compile();
List<Blob> fileBlobs = new ArrayList<>();
for(int x = 0; x < byteFiles.size(); x++){
fileBlobs.add(new javax.sql.rowset.serial.SerialBlob(byteFiles.get(x)));
}
final Blob[] data = fileBlobs.toArray(new Blob[fileBlobs.size()]);
SqlParameterSource in = new MapSqlParameterSource()
.addValue("p_files", new SqlArrayValue<Blob>(data, "BLOB_ARRAY"))
.addValue("p_userId", userId);
Map<String, Object> results = uploadFiles.execute(in);
I created a SQL Type in the DB
create or replace TYPE BLOB_ARRAY is table of BLOB;
Function Spec
FUNCTION insertFiles(p_userId IN NUMBER,
p_files IN BLOB_ARRAY)
RETURN NUMBER;
Function Body
FUNCTION insertFiles (p_userId IN NUMBER,
p_files IN BLOB_ARRAY)
RETURN NUMBER
AS
v_groupId NUMBER := FILE_GROUP_ID_SEQ.NEXTVAL;
v_fileId NUMBER;
BEGIN
FOR i IN 1..p_files.COUNT
LOOP
v_fileId := FILE_ID_SEQ.NEXTVAL;
BEGIN
INSERT INTO FILES
(FILE_ID,
FILE_GROUP_ID,
FILE_DATA,
UPDT_USER_ID)
SELECT
v_fileId,
v_groupId,
p_files(i),
USER_ID
FROM USERS
WHERE USER_ID = p_userId;
EXCEPTION WHEN OTHERS THEN
v_groupId := -1;
END;
END LOOP;
RETURN v_groupId;
END insertFiles;
I am not sure how to correctly pass the array of Blobs to the SQL Function.
Error :
java.sql.SQLException: Fail to convert to internal representation:
javax.sql.rowset.serial.SerialBlob#87829c90 at
oracle.jdbc.oracore.OracleTypeBLOB.toDatum(OracleTypeBLOB.java:69)
~[ojdbc7.jar:12.1.0.1.0] at
oracle.jdbc.oracore.OracleType.toDatumArray(OracleType.java:176)
~[ojdbc7.jar:12.1.0.1.0] at
oracle.sql.ArrayDescriptor.toOracleArray(ArrayDescriptor.java:1321)
~[ojdbc7.jar:12.1.0.1.0] at oracle.sql.ARRAY.(ARRAY.java:140)
~[ojdbc7.jar:12.1.0.1.0] at
UPDATE
After trying Luke's suggestion, I am getting the following error:
uncategorized SQLException for SQL [{? = call FILES_PKG.INSERTFILES(?,
?)}]; SQL state [99999]; error code [22922]; ORA-22922: nonexistent
LOB value ; nested exception is java.sql.SQLException: ORA-22922:
nonexistent LOB value ] with root cause
java.sql.SQLException: ORA-22922: nonexistent LOB value
The error message appears to indicate the Oracle JDBC driver doesn't know what to do with the javax.sql.rowset.serial.SerialBlob object you've passed to it.
Try creating the Blob objects using Connection.createBlob instead. In other words, try replacing the following
loop
for(int x = 0; x < byteFiles.size(); x++){
fileBlobs.add(new javax.sql.rowset.serial.SerialBlob(byteFiles.get(x)));
}
with
Connection conn = dataSource.getConnection();
for(int x = 0; x < byteFiles.size(); x++){
Blob blob = conn.createBlob();
blob.setBytes(1, byteFiles.get(x));
fileBlobs.add(blob);
}
Also, make sure that your parameter names are consistent between your SimpleJdbcCall and your stored function. Your SimpleJdbcCall declares the BLOB array parameter with name p_data but your stored function declaration uses p_files. If the parameter names are not consistent you are likely to get an Invalid column type error.
However, had I run the above test with a stored function of my own that actually did something with the BLOB values passed in, instead of just hard-coding a return value, I might have found that this approach didn't work. I'm not sure why, I'd probably have to spend some time digging around in the guts of Spring to find out.
I tried replacing the Blob values with Spring SqlLobValues, but that didn't work either. I guess Spring's SqlArrayValue<T> type doesn't handle Spring wrapper objects for various JDBC types.
So I gave up on a Spring approach and went back to plain JDBC:
import oracle.jdbc.OracleConnection;
// ...
OracleConnection conn = dataSource.getConnection().unwrap(OracleConnection.class);
List<Blob> fileBlobs = new ArrayList<>();
for(int x = 0; x < byteFiles.size(); x++){
Blob blob = conn.createBlob();
blob.setBytes(1, byteFiles.get(x));
fileBlobs.add(blob);
}
Array array = conn.createOracleArray("BLOB_ARRAY",
fileBlobs.toArray(new Blob[fileBlobs.size()]));
CallableStatement cstmt = conn.prepareCall("{? = call insertFiles(?, ?)}");
cstmt.registerOutParameter(1, Types.NUMERIC);
cstmt.setInt(2, userId);
cstmt.setArray(3, array);
cstmt.execute();
int result = cstmt.getInt(1);
I've tested this with the stored function you've now included in your question, and it is able to call this function and insert the BLOBs into the database.
I'll leave it up to you to do what you see fit with the variable result and to add any necessary cleanup or transaction control.
However, while this approach worked, it didn't feel right. It didn't fit the Spring way of doing things. It did at least prove that what you were asking for was possible, in that there wasn't some limitation in the JDBC driver that meant you couldn't use BLOB arrays. I felt that there ought to be some way to call your function using Spring JDBC.
I spent some time looking into the ORA-22922 error and concluded that the underlying problem was that the Blob objects were being created using a different Connection to what was being used to execute the statement. The question then becomes how to get hold of the Connection Spring uses.
After some further digging around in the source code to various Spring classes, I realised that a more Spring-like way of doing this is to replace the SqlArrayValue<T> class with a different one specialised for BLOB arrays. This is what I ended up with:
import java.sql.Array;
import java.sql.Blob;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.List;
import oracle.jdbc.OracleConnection;
import org.springframework.dao.InvalidDataAccessApiUsageException;
import org.springframework.jdbc.core.support.AbstractSqlTypeValue;
public class SqlBlobArrayValue extends AbstractSqlTypeValue {
private List<byte[]> values;
private String defaultTypeName;
public SqlBlobArrayValue(List<byte[]> values) {
this.values = values;
}
public SqlBlobArrayValue(List<byte[]> values, String defaultTypeName) {
this.values = values;
this.defaultTypeName = defaultTypeName;
}
protected Object createTypeValue(Connection conn, int sqlType, String typeName)
throws SQLException {
if (typeName == null && defaultTypeName == null) {
throw new InvalidDataAccessApiUsageException(
"The typeName is null in this context. Consider setting the defaultTypeName.");
}
Blob[] blobs = new Blob[values.size()];
for (int i = 0; i < blobs.length; ++i) {
Blob blob = conn.createBlob();
blob.setBytes(1, values.get(i));
blobs[i] = blob;
}
Array array = conn.unwrap(OracleConnection.class).createOracleArray(typeName != null ? typeName : defaultTypeName, blobs);
return array;
}
}
This class is heavily based on SqlArrayValue<T>, which is licensed under Version 2 of the Apache License. For brevity, I've omitted comments and a package directive.
With the help of this class, it becomes a lot easier to call your function using Spring JDBC. In fact, you can replace everything after the call to uploadFiles.compile() with the following:
SqlParameterSource in = new MapSqlParameterSource()
.addValue("p_files", new SqlBlobArrayValue(byteFiles, "BLOB_ARRAY"))
.addValue("p_userId", userId);
Map<String, Object> results = uploadFiles.execute(in);
I am trying to implement a function in Oracle 11g which calls a java class to decrypt Blob image information.
Everything seems valid, but I get a ORA-06553 PLS-306 "wrong number or types of arguments"
The function takes a blob and returns a blob so I don't see where the error is coming from.
PL/SQL function:
create or replace
function decrypt_image return blob as
language JAVA name 'Imageutil.decryptBlobImage (java.sqlBlob) return java.sqlBlob';
Java function:
public class Imageutil
public static java.sql.Blob decryptBlobImage (java.sql.Blob img) throws Exception {
try {
int len = (int)img.length();
byte[] imagearray = img.getBytes(1, len);
byte[] decrypted = Encryptor.decryptBinary(imagearray);
Blob retval = new SerialBlob(decrypted);
return retval;
} catch (SQLException ex) {
ex.printStackTrace();
throw new Exception("Error handling blob",ex);
}
}
}
The data is in a table:
temp_image(id number, image blob, decrypted blob);
I am trying to
update temp_image set decrypted = decrypt_image(image);
When I get the error. An Oracle trc file is generated each time, but the there doesn't appear to be an error:
========= Dump for error ORA 1110 (no incident) ========
----- DDE Action: 'DB_STRUCTURE_INTEGRITY_CHECK' (Async) -----
(it then does an integrity check of the database).
The function works, the original data is a long raw, and I can take a hex dump of the data and decrypt it fine. The test table was loaded by a to_lob() function on the original long raw data.
You seem to have java.sqlBlob instead of java.sql.Blob in the PL/SQL declaration; but you also aren't supplying an argument for your function inthat declaraion:
create or replace
function decrypt_image (original_blob blob) return blob as
language JAVA name 'Imageutil.decryptBlobImage (java.sql.Blob) return java.sql.Blob';
Woth your version the PL/SQL function doesn't take an argument, so when you call it as decrypt_image(image) you are passing the wrong number of arguments - it is expecting none but you pass one.
I have a PLSQL code with the following signature.
procedure getall(
p_id in number,
p_code in varchar2,
x_result out tt_objs);
type rt_obj is record(
val1 mytable.attr1%type
val2 mytable.attr2%type
val3 mytable.attr2%type
);
type tt_objs is table of rt_obj index by binary_integer;
What should be the Java code that can invoke this procedure and read x_result?
Maybe this could be what you are looking for.
This should be the interesting part:
//oracle.sql.ARRAY we will use as out parameter from the package
//and will store pl/sql table
ARRAY message_display = null;
//ArrayList to store object of type struct
ArrayList arow= new ArrayList();
//StructDescriptor >> use to describe pl/sql object
//type in java.
StructDescriptor voRowStruct = null;
//ArrayDescriptor >> Use to describe pl/sql table
//as Array of objects in java
ArrayDescriptor arrydesc = null;
//Input array to pl/sql procedure
ARRAY p_message_list = null;
//Oracle callable statement used to execute procedure
OracleCallableStatement cStmt=null;
try
{
//initializing object types in java.
voRowStruct = StructDescriptor.createDescriptor("RECTYPE",conn);
arrydesc = ArrayDescriptor.createDescriptor("RECTAB",conn);
}
catch (Exception e)
{
throw OAException.wrapperException(e);
}
for(XXVORowImpl row = (XXVORowImpl)XXVO.first();
row!=null;
row = (XXVORowImpl)XXVO.next())
{
//We have made this method to create struct arraylist
// from which we will make ARRAY
//the reason being in java ARRAY length cannot be dynamic
//see the method defination below.
populateObjectArraylist(row,voRowStruct,arow);
}
//make array from arraylist
STRUCT [] obRows= new STRUCT[arow.size()];
for(int i=0;i < arow.size();i++)
{
obRows[i]=(STRUCT)arow.get(i);
}
try
{
p_message_list = new ARRAY(arrydesc,conn,obRows);
}
catch (Exception e)
{
throw OAException.wrapperException(e);
}
//jdbc code to execute pl/sql procedure
try
{
cStmt
=(OracleCallableStatement)conn.prepareCall("{CALL ioStructArray.testproc(:1,:2)}");
cStmt.setArray(1,p_message_list);
cStmt.registerOutParameter(2,OracleTypes.ARRAY,"RECTAB");
cStmt.execute();
//getting Array back
message_display = cStmt.getARRAY(2);
//Getting sql data types in oracle.sql.datum array
//which will typecast the object types
Datum[] arrMessage = message_display.getOracleArray();
//getting data and printing it
for (int i = 0; i < arrMessage.length; i++)
{
oracle.sql.STRUCT os = (oracle.sql.STRUCT)arrMessage[i];
Object[] a = os.getAttributes();
System.out.println("a [0 ] >>attribute1=" + a[0]);
System.out.println("a [1 ] >>attribute2=" + a[1]);
System.out.println("a [2 ] >>attribute3=" + a[2]);
Yep, it's not possible directly. You can either
Create a public type with the same structure as the PLSQL record and follow the Eggi's advice. Similar approach uses Oracle JPublisher. JPublisher can help you to automate this process.
Or you can use anonymous PLSQL block to create or read PLSQL records. We are thinking about creating a library do it automatically in our company.
Or you can create a wrapper functions to wrap records to XML (both in Java and PLSQL). Then pass XML as Xmltype or CLOB between DB and Java. We have already this solution for some complicated structures. It's tedious and slows down a processing a little bit, but it works.