views:

289

answers:

3

I have an application using hibernate 3.1 and JPA annotations. It has a few objects with byte[] attributes (1k - 200k in size). It uses the JPA @Lob annotation, and hibernate 3.1 can read these just fine on all major databases -- it seems to hide the JDBC Blob vendor peculiarities (as it should do).

@Entity
public class ConfigAttribute {
  @Lob
  public byte[] getValueBuffer() {
    return m_valueBuffer;
  }
}

We had to upgrade to 3.5, when we discovered that hibernate 3.5 breaks (and won't fix) this annotation combination in postgresql (with no workaround). I have not found a clear fix so far, but I did notice that if I just remove the @Lob, it uses the postgresql type bytea (which works, but only on postgres).

annotation                   postgres     oracle      works on
-------------------------------------------------------------
byte[] + @Lob                oid          blob        oracle
byte[]                       bytea        raw(255)    postgresql
byte[] + @Type(PBA)          oid          blob        oracle
byte[] + @Type(BT)           bytea        blob        postgresql

once you use @Type, @Lob seems to not be relevant
note: oracle seems to have deprecated the "raw" type since 8i.

I am looking for a way to have a single annotated class (with a blob property) which is portable across major databases.

  • What is the portable way to annotate a byte[] property?
  • Is this fixed in some recent version of hibernate?

Update: After reading this blog I have finally figured out what the original workaround in the JIRA issue was: Apparently you are supposed to drop @Lob and annotate the property as:

@Type(type="org.hibernate.type.PrimitiveByteArrayBlobType") 
byte[] getValueBuffer() {...

However, this does not work for me -- I still get OIDs instead of bytea; it did however work for the author of the JIRA issue, who seemed to want oid.

After the answer from A. Garcia, I then tried this combo, which actually does work on postgresql, but not on oracle.

@Type(type="org.hibernate.type.BinaryType") 
byte[] getValueBuffer() {...

What I really need to do is control which @org.hibernate.annotations.Type the combination (@Lob + byte[] gets mapped) to (on postgresql).


Here is the snippet from 3.5.5.Final from MaterializedBlobType (sql type Blob). According to Steve's blog, postgresql wants you to use Streams for bytea (don't ask me why) and postgresql's custom Blob type for oids. Note also that using setBytes() on JDBC is also for bytea (from past experience). So this explains why use-streams has no affect they both assume 'bytea'.

public void set(PreparedStatement st, Object value, int index) {
 byte[] internalValue = toInternalFormat( value );
 if ( Environment.useStreamsForBinary() ) {
  // use streams = true
   st.setBinaryStream( index, 
    new ByteArrayInputStream( internalValue ), internalValue.length );
 }
 else {
  // use streams = false
  st.setBytes( index, internalValue );
 }
}

This results in:

ERROR: column "signature" is of type oid but expression is of type bytea

Update The next logical question is: "why not just change the table definitions manually to bytea" and keep the (@Lob + byte[])? This does work, UNTIL you try to store a null byte[]. Which the postgreSQL driver thinks is an OID type expression and the column type is bytea -- this is because hibernate (rightly) calls JDBC.setNull() instead of JDBC.setBytes(null) which PG driver expects.

ERROR: column "signature" is of type bytea but expression is of type oid

The type system in hibernate is currently a 'work in progress' (according to 3.5.5 deprecation comment). In fact so much of the 3.5.5 code is deprecated, it is hard to know what to look at when sub-classing the PostgreSQLDialect).

AFAKT, Types.BLOB/'oid' on postgresql should be mapped to some custom type which uses OID style JDBC access (i.e. PostgresqlBlobType object and NOT MaterializedBlobType). I've never actually successfully used Blobs with postgresql, but I do know that bytea just simply works as one / I would expect.

I am currently looking at the BatchUpdateException -- its possible that the driver doesn't support batching.


Great quote from 2004: "To sum up my ramblings, I'd say they we should wait for the JDBC driver to do LOBs properly before changing Hibernate."

References:

+3  A: 

I have finally got this working. It expands on the solution from A. Garcia, however, since the problem lies in the hibernate type MaterializedBlob type just mapping Blob > bytea is not sufficient, we need a replacement for MaterializedBlobType which works with hibernates broken blob support. This implementation only works with bytea, but maybe the guy from the JIRA issue who wanted OID could contribute an OID implementation.

Sadly replacing these types at runtime is a pain, since they should be part of the Dialect. If only this JIRA enhanement gets into 3.6 it would be possible.

public class PostgresqlMateralizedBlobType extends AbstractSingleColumnStandardBasicType<byte[]> {
 public static final PostgresqlMateralizedBlobType INSTANCE = new PostgresqlMateralizedBlobType();

 public PostgresqlMateralizedBlobType() {
  super( PostgresqlBlobTypeDescriptor.INSTANCE, PrimitiveByteArrayTypeDescriptor.INSTANCE );
 }

  public String getName() {
   return "materialized_blob";
  }
}

Much of this could probably be static (does getBinder() really need a new instance?), but I don't really understand the hibernate internal so this is mostly copy + paste + modify.

public class PostgresqlBlobTypeDescriptor extends BlobTypeDescriptor implements SqlTypeDescriptor {
  public static final BlobTypeDescriptor INSTANCE = new PostgresqlBlobTypeDescriptor();

  public <X> ValueBinder<X> getBinder(final JavaTypeDescriptor<X> javaTypeDescriptor) {
   return new PostgresqlBlobBinder<X>(javaTypeDescriptor, this);
  }
  public <X> ValueExtractor<X> getExtractor(final JavaTypeDescriptor<X> javaTypeDescriptor) {
   return new BasicExtractor<X>( javaTypeDescriptor, this ) {
    protected X doExtract(ResultSet rs, String name, WrapperOptions options) throws SQLException { 
      return (X)rs.getBytes(name);
    }
   };
  }
}

public class PostgresqlBlobBinder<J> implements ValueBinder<J> {
 private final JavaTypeDescriptor<J> javaDescriptor;
 private final SqlTypeDescriptor sqlDescriptor;

 public PostgresqlBlobBinder(JavaTypeDescriptor<J> javaDescriptor, SqlTypeDescriptor sqlDescriptor) { 
  this.javaDescriptor = javaDescriptor; this.sqlDescriptor = sqlDescriptor;
 }  
 ...
 public final void bind(PreparedStatement st, J value, int index, WrapperOptions options) 
 throws SQLException {
  st.setBytes(index, (byte[])value);
 }
}
Justin
@Justin +1 for your research. Congratulations. Just an advice: Prefer to edit your own question/answer up to 8 times. Otherwise, your question/answer *will become community wiki* and you will not gain reputation and UP vote will not be computed anymore
Arthur Ronald F D Garcia
Live and learn I suppose, I had so many edits, as I kept forgetting to do one thing or other with my test environment.
Justin
Same here, +1 for the research and *a* solution for your situation.
Pascal Thivent
+3  A: 

Here goes what O'reilly Enterprise JavaBeans, 3.0 says

JDBC has special types for these very large objects. The java.sql.Blob type represents binary data, and java.sql.Clob represents character data.

Here goes PostgreSQLDialect source code

public PostgreSQLDialect() {
    super();
    ...
    registerColumnType(Types.VARBINARY, "bytea");
    /**
      * Notice it maps java.sql.Types.BLOB as oid
      */
    registerColumnType(Types.BLOB, "oid");
}

So what you can do

Override PostgreSQLDialect as follows

public class CustomPostgreSQLDialect extends PostgreSQLDialect {

    public CustomPostgreSQLDialect() {
        super();

        registerColumnType(Types.BLOB, "bytea");
    }
}

Now just define your custom dialect

<property name="hibernate.dialect" value="br.com.ar.dialect.CustomPostgreSQLDialect"/>

And use your portable JPA @Lob annotation

@Lob
public byte[] getValueBuffer() {

UPDATE

Here has been extracted here

I have an application running in hibernate 3.3.2 and the applications works fine, with all blob fields using oid (byte[] in java)

...

Migrating to hibernate 3.5 all blob fields not work anymore, and the server log shows: ERROR org.hibernate.util.JDBCExceptionReporter - ERROR: column is of type oid but expression is of type bytea

which can be explained here

This generaly is not bug in PG JDBC, but change of default implementation of Hibernate in 3.5 version. In my situation setting compatible property on connection did not helped.

...

Much more this what I saw in 3.5 - beta 2, and i do not know if this was fixed is Hibernate - without @Type annotation - will auto-create column of type oid, but will try to read this as bytea

Interesting is because when he maps Types.BOLB as bytea (See CustomPostgreSQLDialect) He get

Could not execute JDBC batch update

when inserting or updating

Arthur Ronald F D Garcia
This solution looks glorious, I am trying it now.
Justin
This generates the correct DDL, but fails at runtime: I get a java.sql.BatchUpdateException when trying to an object with a blob property.
Justin
@Justin Try a similar scenario by using Oracle instead of PostgreSQL and see what you get. BatchUpdateException has to do with errors that occurs during a batch update operation.
Arthur Ronald F D Garcia
Actually what I really want is not to map BLOB to "bytea" but instead map (byte[] + @Lob) annotation combination to Types.VARBINARY!
Justin
@Justin See http://download-llnw.oracle.com/javase/1.5.0/docs/api/java/sql/BatchUpdateException.html
Arthur Ronald F D Garcia
@Justin Interesting, Although i do not use Postgres, PostgreSQLDialect maps standard *Types.VARBINARY* as **bytea**. So i think Postgres does not support native SQL Types.VARBINARY. See http://download-llnw.oracle.com/javase/1.5.0/docs/api/java/sql/Types.html#VARBINARY It has been clear: **It identifies the generic SQL type VARBINARY**
Arthur Ronald F D Garcia
@Justin See **UPDATE**
Arthur Ronald F D Garcia
Thanks for the research, I am currently trying all combinations of drivers and some variations of hibernate. I even checked the value of `Environment.useStreamsForBinary()` to make sure I was setting the global property.
Justin
+1 for actually providing very good hints for a solution.
Pascal Thivent
+6  A: 

What is the portable way to annotate a byte[] property?

It depends on what you want. JPA can persist a non annotated byte[]. From the JPA 2.0 spec:

11.1.6 Basic Annotation

The Basic annotation is the simplest type of mapping to a database column. The Basic annotation can be applied to a persistent property or instance variable of any of the following types: Java primitive, types, wrappers of the primitive types, java.lang.String, java.math.BigInteger, java.math.BigDecimal, java.util.Date, java.util.Calendar, java.sql.Date, java.sql.Time, java.sql.Timestamp, byte[], Byte[], char[], Character[], enums, and any other type that implements Serializable. As described in Section 2.8, the use of the Basic annotation is optional for persistent fields and properties of these types. If the Basic annotation is not specified for such a field or property, the default values of the Basic annotation will apply.

And Hibernate will map a it "by default" to a SQL VARBINARY (or a SQL LONGVARBINARY depending on the Column size?) that PostgreSQL handles with a bytea.

But if you want the byte[] to be stored in a Large Object, you should use a @Lob. From the spec:

11.1.24 Lob Annotation

A Lob annotation specifies that a persistent property or field should be persisted as a large object to a database-supported large object type. Portable applications should use the Lob annotation when mapping to a database Lob type. The Lob annotation may be used in conjunction with the Basic annotation or with the ElementCollection annotation when the element collection value is of basic type. A Lob may be either a binary or character type. The Lob type is inferred from the type of the persistent field or property and, except for string and character types, defaults to Blob.

And Hibernate will map it to a SQL BLOB that PostgreSQL handles with a oid .

Is this fixed in some recent version of hibernate?

Well, the problem is that I don't know what the problem is exactly. But I can at least say that nothing has changed since 3.5.0-Beta-2 (which is where a changed has been introduced)in the 3.5.x branch.

But my understanding of issues like HHH-4876, HHH-4617 and of PostgreSQL and BLOBs (mentioned in the javadoc of the PostgreSQLDialect) is that you are supposed to set the following property

hibernate.jdbc.use_streams_for_binary=false

if you want to use oid i.e. byte[] with @Lob (which is my understanding since VARBINARY is not what you want with Oracle). Did you try this?

As an alternative, HHH-4876 suggests using the deprecated PrimitiveByteArrayBlobType to get the old behavior (pre Hibernate 3.5).

References

  • JPA 2.0 Specification
    • Section 2.8 "Mapping Defaults for Non-Relationship Fields or Properties"
    • Section 11.1.6 "Basic Annotation"
    • Section 11.1.24 "Lob Annotation"

Resources

Pascal Thivent
OMG, I realize this question has changed a lot since I started answering. Will read all the changes later and update my answes after digesting the changes if necessary.
Pascal Thivent
Its good to see the spec, so hibernate is totally correct to map (@Lob + byte[]) to a large object supported type. In Postgresql there are 2 (bytea or oid). However, while hibernate 3.5 maps to oid (by default) it reads using JDBC getBytes() which PGSQL driver returns the 6 byte oid instead of the data. Note also that the blog author has responded most helpfully (on his blog) since the question was posed.
Justin
@Justin *However, while hibernate 3.5 maps to oid (by default) it reads using JDBC getBytes() which PGSQL driver returns the 6 byte oid instead of the data* - does this occur when using `hibernate.jdbc.use_streams_for_binary=false` too? (going to check what Steve said now).
Pascal Thivent
I am going to try specifying it in the properties file, however PostgreSQLDialect has useInputStreamToInsertBlob() returning false so I assume that I am -- since I am not explicitly setting this property.
Justin
After setting this property (to either true or false), I get a runtime exception: ERROR: column "signature" is of type bytea but expression is of type oid". I should mention I am using hibernate 3.5.5.Final + PG 8.2 drivers.
Justin
@Justin Hmm... Why is the error reporting about bytea? What did you try exactly? `@Lob byte[]` + `hibernate.jdbc.use_streams_for_binary=false`?
Pascal Thivent
This seems strange to me as well -- i double checked the db (no bytea anywhere) I need to hunt down the source of that message. I've tried streams both false and true (@Lob + byte[]). All give the same error when saving an object with a blob. Only thing that works seems to be @Type(type="org.hibernate.type.BinaryType") when using bytea.
Justin
@Justin `All give the same error when saving an object with a blob.` This is very disappointing given that it's the advice given by Hibernate developers :S. `Only thing that works seems to be @Type(type="org.hibernate.type.BinaryType") when using bytea` This is what you'd get without the @Lob. But it won't work with Oracle, right? I think you'll have to customize the Dialect for a portable solution.
Pascal Thivent
Either that or my setup is messed up, perhaps I need to up the version of hibernate or postgres. I tried with both 8.2 and 8.4 drivers -- nothing so far :(
Justin
@Justin Not sure upgrading Hibernate beyond 3.5.0-Beta-2 will change anything. And if I understand things correctly, Postgres introduced this change in 7.2. What I don't get is the initial recommendation of Steve blog post as it doesn't seem to work (and in the current state, it seems that the change made in Hibernate does break something that was working). I must be missing something...
Pascal Thivent
My updated question explains why it does not work. Do you know if there is a hibernate test matrix? Unless his blog is talking about a different release of hibernate, the advice does not seem to have been tested.
Justin
@Justin I'll read all this, including the blog post, carefully tonight. But to my knowledge, the blog is talking about Hibernate 3.5.0-Beta-2+ and the changes introduced by [HHH-4405](http://opensource.atlassian.com/projects/hibernate/browse/HHH-4405) and [HHH-3892](http://opensource.atlassian.com/projects/hibernate/browse/HHH-3892) so I'm tempted to think that you are right. Honestly, I still don't understand all the reasons for these changes and, somehow, they seem to break something.
Pascal Thivent