Hibernate database specific columnDefinition values - mysql

the problem is as follows: We're using hibernate with annotations as O/R Mapper.
Some #Column annotations look like:
#Column(columnDefinition = "longblob", name = "binaryData", nullable = true)
or
#Column(columnDefinition = "mediumtext", name = "remark", nullable = true)
with the columnDefinition attributes being mysql specific
on postgres for example, the columnDefinition values should be "bytea" and "varchar(999999)"
and on oracle probably something else.
Problems arise currently at the time of Schema Export, e.g. when creating the DDL statements.
Possible workarounds that I can think of are
- Hack some JDBC driver that does a text replace (e.g. longblob->bytea) for the DDL statements. That's ugly but will work somehow
- Use hibernate xml configuration instead of annotations. That will probably work but I prefer annotations
Does anybody know any alternatives? Hibernate specific workarounds are ok, e.g. if the columnDefinition attribute can contain dialect specific values like
#Column(columnDefinition = "mysql->mediumtext, postgres->varchar(999999)", name = "remark", nullable = true)
Thanks
Holger

Why don't you use the database-agnostic annotations like:
#Lob (on a byte[] or a String property)
#Column(length=90000) (on a String property)
and see what columns will be generated in the database. They will most likely be of the types you want them to be.

Some ideas:
Use annotation in general, but overload them from Xml in the case where they are specific to your database. Then you can have one configuration file specific to your database.
Use java Constants in your annotations (they have to be compile-time constants, so you are limited). You can have several sets of Java constants, and point toward the one you want to export to. (Beware, when you point toward another constant, you have to recompile everything.)
I have also used the dialect to switch some code in my Configuration class. The configuration class receives all data (from annotations or xml), and can then postprocess it.
For example, I have changed the concatenation symbol from '||' on Oracle to '+' on SqlServer.
This is conveniently done at runtime :-)

Related

Is there any way to update SET type column using JPA query?

What I want to do is update field in table by JPA query.
I set my MySQL table with SET type.
create table staff (
id BIGINT,
...
roles SET('A', 'B', 'C') not null,
...
)
and class
public class Staff {
private Long id;
#Converter(someConverterHere.class)
private Set<Role> roles;
}
and I tried to update this field like the way below
#Modifying
#Query("update Staff s SET s.roles = :newRoles where s.id = :id")
Integer updateStaffRoles(#Param("id") Long id, #Param("newRoles") Set<Role> newRoles);
Then, When I gave this Set.of(Role.A, Role.B) as argument, it showed me the error like this,
Caused by: java.lang.IllegalArgumentException: Parameter value [A] did not match expected type [java.util.Set (n/a)]
at org.hibernate.query.spi.QueryParameterBindingValidator.validate(QueryParameterBindingValidator.java:54)
at org.hibernate.query.spi.QueryParameterBindingValidator.validate(QueryParameterBindingValidator.java:27)
at org.hibernate.query.internal.QueryParameterBindingImpl.validate(QueryParameterBindingImpl.java:90)
at org.hibernate.query.internal.QueryParameterBindingImpl.setBindValue(QueryParameterBindingImpl.java:55)
at org.hibernate.query.internal.QueryParameterBindingsImpl.expandListValuedParameters(QueryParameterBindingsImpl.java:636)
at org.hibernate.query.internal.AbstractProducedQuery.doExecuteUpdate(AbstractProducedQuery.java:1629)
at org.hibernate.query.internal.AbstractProducedQuery.executeUpdate(AbstractProducedQuery.java:1612)
at org.springframework.data.jpa.repository.query.JpaQueryExecution$ModifyingExecution.doExecute(JpaQueryExecution.java:238)
at org.springframework.data.jpa.repository.query.JpaQueryExecution.execute(JpaQueryExecution.java:88)
at org.springframework.data.jpa.repository.query.AbstractJpaQuery.doExecute(AbstractJpaQuery.java:154)
at org.springframework.data.jpa.repository.query.AbstractJpaQuery.execute(AbstractJpaQuery.java:142)
at org.springframework.data.repository.core.support.QueryExecutorMethodInterceptor$QueryMethodInvoker.invoke(QueryExecutorMethodInterceptor.java:195)
at org.springframework.data.repository.core.support.QueryExecutorMethodInterceptor.doInvoke(QueryExecutorMethodInterceptor.java:152)
at org.springframework.data.repository.core.support.QueryExecutorMethodInterceptor.invoke(QueryExecutorMethodInterceptor.java:130)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
at org.springframework.data.projection.DefaultMethodInvokingMethodInterceptor.invoke(DefaultMethodInvokingMethodInterceptor.java:80)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:367)
at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:118)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
at org.springframework.dao.support.PersistenceExceptionTranslationInterceptor.invoke(PersistenceExceptionTranslationInterceptor.java:139)
... 81 common frames omitted
It seems jpa query builder validates all single value in newRoles and compare its type with type of Staff.roles, which cannot be true (Role and Set)
Is there any way to solve this? I was not able to find any solution for this.
dependency
org.springframework.data:spring-data-jpa:2.3.2.RELEASE
=====================================================
Temporal solution:
I changed paramter type from Set to Set<Set> and it works. And now, This problem becomes problem when using QueryDsl. Using "set" of querydsl, this problem happens again :(
Querydsl
// newRoles : Set<Role>
// Error: IllegalArgumentException
query.set(QStaff.Staff.roles, newRoles));
// Of course, compile error
query.set(QStaff.Staff.roles, Set.of(newRoles)));
Custom types need to be mapped by a custom type in your JPA implementation. For Hibernate that is through the User Type interface. In order to use extended types through QueryDSL, you need to create custom expression types for it. Furthermore, for any of the operations you want to support (like set contains, equality), you will have to register custom functions. It can be done, but not using the regular API's. You're probably best off denormalizing your metamodel and using an #ElementCollection instead. However, if you insist on getting to work, here are some pointers:
hibernate-types is a widely popular project that implements many custom types for Hibernate. It doesn't implement SET, but you can probably use the implementation for arrays as basis.
hibernate-types-querydsl-apt is a project that extends metamodel generation of QueryDSL for some of the custom types. You should probably mimic anything in that repository for sets as well.

JPA Hibernate - Multiple Database Dialects and nvarchar(length) data type

I have to do a project using JPA + Hibernate in which I'm using 3 dialects: MySQL5InnoDBDialect, MSSQL2012Dialect and Oracle12cDialect.
Right now I have a specification which is telling me that for some column from:
Oracle database, I have to use NVARCHAR2(LENGTH) data type
MySql database, I have to use VARCHAR(LENGTH) data type
MSSQL database, I have to use NVARCHAR(LENGTH) data type
... and here is my problem..
If I use:
#Column(name="columnName" length = 255)
private String columnName;
hibernate generates varchar(255) and this is good just for MySQL
If I use:
#Column(name="columnName", columnDefinition="nvarchar2(255)")
private String columnName;
it's not possible in MySQL, i get error because of columnDefinition, but in oracle is okay
I tried to customize MySQL dialect creating
public class CustomMySQL5InnoDBDialect extends MySQL5InnoDBDialect{
public CustomMySQL5InnoDBDialect() {
super();
registerColumnType(Types.NVARCHAR, "nvarchar2($l)");//$l not $1
registerHibernateType(Types.NVARCHAR, StandardBasicTypes.STRING.getName());
}
}
and giving this class in hibernate configuration for MySQL dialect.
I have the same problem in MySQL if I'm using columnDefinition property.
Can you help with this problem please?
The solution is to make use of the feature that the JPA API spec provides you with for just this situation. Define a file orm.xml for each datastore that you need to support, and enable the requisite one when using each database. See this link for details of the file format. That way you don't need to think about hacking the internal features of whichever JPA provider you are using, and you also retain JPA provider portability, as well as database portability
The idea of putting schema specific information info (static) Java annotations is an odd one, even more so when wanting database portability.

JDO Class - convert to varchar or nvarchar based on MySQL or MSSQL

I have a JDO Class. Some of the attributes are as shown below:
#Column(jdbcType = "VARCHAR", length = 200)
String anotherSrcFieldValue;
#Column(jdbcType = "BIGINT")
long tgtFieldId;
#Column(jdbcType = "VARCHAR", length = 200)
String tgtFieldValue;
With MySQL and MSSQL it works fine.
My requirement is, if it is MySQL make it a column of type VARCHAR; and when it is MSSQL, create a column of type NVARCHAR. How can I achieve this?
A second requirement is one entity class to be run on both the databases.
All JDO docs I've seen explain clearly that putting schema specific info in annotations is a bad idea. Consequently you should have 2 files "package-mysql.orm" and "package-mssql.orm" to specify the schema-specific parts of the mapping, and set "datanucleus.Mapping" to be either "mysql" or "mssql" depending on your datastore. As per http://www.datanucleus.org/products/accessplatform_4_2/jdo/orm/metadata_orm.html

Tell Hibernate's hbm2ddl to add MySQL enum columns for #Enumerated annotated fields

I'm creating a DB table using hbm2ddl with Java code similar to the following:
#Entity
public class Filter {
public enum Type {
TypeA, TypeB;
}
#Enumerated(EnumType.STRING)
private Type type;
}
It works fine, but for "type" a VARCHAR column is created, i.e. the DDL code looks like this:
CREATE TABLE IF NOT EXISTS `filter` (`type` varchar(255) DEFAULT NULL)
But what I want to have is this:
CREATE TABLE IF NOT EXISTS `filter` (`type` enum('TypeA','TypeB') NOT NULL)
Is this possible to declare in Hibernate, preferred with annotations?
Or is there a way to extend SchemaUpdate and overwrite the method that renders the alter script part for enumerated field the way I like it?
Background: The same database is used in a PHP part of the project and I want to prevent that invalid values are inserted.
Although it seems there is no way to handle MySQL enums 100% automatically, as pointed out by Lucas on his answer, there is actually a simple way to contour it. You may use columnDefinition attribute on #Column annotation, which seems to be specifically designed to generate custom DDL code.
See the documentation excerpt describing the attribute:
(Optional) The SQL fragment that is used when generating the DDL for the column.
Defaults to the generated SQL to create a column of the inferred type.
The NOT NULL restriction is quite standard, and is supported by another attribute nullable.
Thus, your property definition would look like this:
#Enumerated(EnumType.STRING)
#Column(columnDefinition = "enum ('TypeA', 'TypeB')", nullable = false)
private Type type;
I believe that's going to be complicated, since the java.sql.Types, which define the sql types treated by java, does not have enum type (since it's not a standardized type according to SQL-92).
If that was the case you could create a hibernate custom UserType extending the EnumType and setting the sqlType accordingly, but since java.sql.Types doesn't handle it I don't see how to use native sql enum.
best regards!

Setting column length of a Long value with JPA annotations

I'm performing a little database optimisation at the moment and would like to set the column lengths in my table through JPA. So far I have no problem setting the String (varchar) lengths using JPA as follows:
#Column(unique=true, nullable=false, length=99)
public String getEmail() {
return email;
}
However, when I want to do the same for a column which is of type Long (bigint), it doesn't work. For example, if I write:
#Id
#Column(length=7)
#GeneratedValue(strategy = GenerationType.AUTO)
public Long getId() {
return id;
}
The column size is still set as the default of 20. Are we able to set these lengths in JPA or am I barking up the wrong tree?
Thanks,
Gearoid.
precision, scale make more sense for a numeric type. Also depends whether the JDBC driver and RDBMS allows setting of those on the particular column type
Are you using Eclipse as your IDE? If so, I suggest you make use of the Dali plugin (already installed) and active the "JPA Details" view. This view will help guide you as to what attributes you need to plug into your annotations.
You could have scale=7 instead of length=7. However JPA is still going to tell the database to prepare to hold an int of scale 20.
Also make sure you have your database dialect properly set.