Using MySQL "SET" column with Ebean ORM - java

I'm trying to recreate one of our web apps using Play 2.0 with Ebean and I've hit a road block. I can't figure out how to map MySQL's SET type to a field in the model. I've gotten ENUM columns working using the #Enumerated(EnumType.STRING) annotation but I can't seem to find any info on SET columns.
The table mimics a crontab:
CREATE TABLE IF NOT EXISTS `schedule` (
`id` int(10) unsigned NOT NULL auto_increment,
`task_id` mediumint(8) unsigned NOT NULL default '0',
`month` set('January','February','March','April','May','June','July','August','September','October','November','December') default NULL,
`mday` set('1','2','3','4','5','6','7','8','9','10','11','12','13','14','15','16','17','18','19','20','21','22','23','24','25','26','27','28','29','30','31','-1','-2','-3','-4','-5','-6','-7','-8','-9','-10') default NULL,
`wday` set('Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday') default NULL,
`hour` set('0','1','2','3','4','5','6','7','8','9','10','11','12','13','14','15','16','17','18','19','20','21','22','23') default NULL,
`minute` set('00','01','02','03','04','05','06','07','08','09','10','11','12','13','14','15','16','17','18','19','20','21','22','23','24','25','26','27','28','29','30','31','32','33','34','35','36','37','38','39','40','41','42','43','44','45','46','47','48','49','50','51','52','53','54','55','56','57','58','59') default NULL,
`updated` timestamp NOT NULL default CURRENT_TIMESTAMP on update CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `event` (`task_id`)
)
I've now created a UserType and related annotations as suggested by MvG:
#Entity
public class Schedule extends Model {
public enum Month { JANUARY, FEBRUARY, MARCH, APRIL, MAY, JUNE, JULY, AUGUST, SEPTEMBER, OCTOBER, NOVEMBER, DECEMBER };
public enum Weekday { SUNDAY, MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY };
#Id
public Long id;
#ManyToOne(cascade = CascadeType.MERGE)
public Task task;
#Version
public Timestamp updated;
#Type(type="models.EnumSetUserType",parameters=#Parameter(name="enumType",value="models.Schedule$Month"))
#Column(name="month", columnDefinition="SET('JANUARY','FEBRUARY','MARCH','APRIL','MAY','JUNE','JULY','AUGUST','SEPTEMBER','OCTOBER','NOVEMBER','DECEMBER')")
#MonthEnum
public EnumSet<Month> months;
#Type(type="models.IntegerSetUserType")
#IntegerSet(min=-30,max=30)
#Column(name="mday",columnDefinition="SET('1','2','3','4','5','6','7','9','10','11','12','13','14','15','16','17','18','19','20','21','22','23','24','25','26','27','28','29','30','31','-1','-2','-3','-4','-5','-6','-7','-8','-9')")
public Set<Integer> mdays;
#Type(type="models.EnumSetUserType", parameters = #Parameter(name="enumType", value="models.Schedule$Weekday"))
#Column(name="wday", columnDefinition="SET('MONDAY','TUESDAY','WEDNESDAY','THURSDAY','FRIDAY','SATURDAY')")
#WeekdayEnum
public EnumSet<Weekday> weekdays;
#Type(type="models.IntegerSetUserType")
#IntegerSet(min=0,max=23)
#Column(name="hour",columnDefinition="SET('0','1','2','3','4','5','6','7','9','10','11','12','13','14','15','16','17','18','19','20','21','22','23')")
public Set<Integer> hours;
#Type(type="models.IntegerSetUserType")
#IntegerSet(min=0,max=59)
#Column(name="minute",columnDefinition="SET('00','01','02','03','04','05','06','07','08','09','10','11','12','13','14','15','16','17','18','19','20','21','22','23','24','25','26','27','28','29','30','31','32','33','34','35','36','37','38','39','40','41','42','43','44','45','46','47','48','49','50','51','52','53','54','55','56','57','58','59')")
public Set<Integer> minutes;
public static Finder<Long,Schedule> find = new Finder<Long,Schedule>(Long.class, Schedule.class);
}
EnumSetUserType:
public class EnumSetUserType<E extends Enum<E>> implements UserType, ParameterizedType, Serializable {
private Class<? extends EnumSet> clazz = null;
private Class<E> enum_type = null;
#Override
public void setParameterValues(Properties parameters) {
String enum_class_name = parameters.getProperty("enumType");
try {
enum_type = ReflectHelper.classForName(parameters.getProperty("enumType"), this.getClass()).asSubclass(Enum.class);
//enum_type = (Class<E>) Class.forName(enum_class_name);
//enum_type = (Class<E>) Play.application().classloader().loadClass(enum_class_name);
}
catch (ClassNotFoundException e) {
throw new HibernateException("enum class " + enum_class_name + " not found", e);
}
}
#Override
public Object nullSafeGet(ResultSet rs, String[] column_names, SessionImplementor session, Object owner) throws HibernateException, SQLException {
String value_str = rs.getString(column_names[0]);
System.out.println("getting " + column_names[0] + " using " + getClass());
if (rs.wasNull())
return null;
List<E> enum_values = new ArrayList<E>();
for (String value : value_str.split(","))
enum_values.add(Enum.valueOf(enum_type, value));
return EnumSet.copyOf(enum_values);
}
#Override
public void nullSafeSet(PreparedStatement statement, Object object, int index, SessionImplementor session) throws HibernateException, SQLException {
System.out.println("Setting " + index + " to " + object + " using " + getClass());
if (object == null) {
statement.setNull(index, Types.VARCHAR);
return;
}
Set<E> values = (Set<E>) object;
StringBuilder sb = new StringBuilder();
for (E value : values)
sb.append(value.name()).append(",");
System.out.println("Setting " + index + " to " + sb.length() + " using " + getClass());
statement.setString(index, sb.substring(0, sb.length() - 1));
}
#Override
public int[] sqlTypes() {
return new int[] { Types.VARCHAR };
}
#Override
public Class returnedClass() {
return clazz;
}
#Override
public Object deepCopy(Object value) throws HibernateException {
return value;
}
#Override
public boolean isMutable() {
return false;
}
#Override
public Object replace(Object original, Object target, Object owner) throws HibernateException {
return original;
}
#Override
public Object assemble(Serializable cached, Object owner) throws HibernateException {
return cached;
}
#Override
public Serializable disassemble(Object value) throws HibernateException {
return (Serializable) value;
}
#Override
public boolean equals(Object x, Object y) throws HibernateException {
if (x == y)
return true;
if (x == null || y == null)
return false;
return x.equals(y);
}
#Override
public int hashCode(Object x) throws HibernateException {
return x.hashCode();
}
}
It no longer reports any errors, but even when I manually populate and save a Schedule in a controller action, the Set fields aren't saved in the database. How can I get Play+Ebean to work with this table?

If you map an ENUM column to a Java Enumeration, you should probably map a SET column to a Java EnumSet. There is at least one question on SF about mapping EnumSet, but the solution there appears to be a separate table, not the mysql SET type.
It also appears that there is no support in hybernate for mysql SET types. Therefore, you'll have to write your own UserType. I'm not sure about the difference, but it seems you could easily make this an EnhancedUserType, which would probably make your implementation more versatile. If your project allows using LGPL-licensed source code, you might use the implementation of EnumType as a template for your own EnumSetType. Adapting that should be easy, particularly as you could throw away all the “save as ordinal” parts of the code.
Once you have your own UserType for EnumSet, you could annotate the corresponding fields with that #Type. Or in the hibernation configuration, if there is such a thing in your setup. There might be even ways to register the type somewhere, to automatically map all EnumSet instances using that type, but I know too little about this whole hibernation stuff to decide whether this is desirable or even possible. I haven't even figured out yet how the #Enumerated annotation maps to the EnumType implementation.
With the right keywords (UserType EnumSet split), one can find some implementations on the web. So you wouldn't even have to write your own code, but could simply include one of these solutions. Some come with a short description about how to use them.

Related

Hibernate CompositeUserType that is comparible in JPA-QL (or HQL) query

I've created a custom type for Hibernate to store an OffsetDateTime's timestamp and offset (because the default JPA 2.2 / Hibernate 5.2 with java 8 support implementation loses the offset information):
public class OffsetDateTimeHibernateType implements CompositeUserType {
#Override
public Class returnedClass() {
return OffsetDateTime.class;
}
#Override
public String[] getPropertyNames() {
return new String[] {"dateTime", "zoneOffset"};
}
#Override
public Type[] getPropertyTypes() {
// Not sure if we should use LocalDateTimeType.INSTANCE instead of TIMESTAMP
return new Type[]{StandardBasicTypes.TIMESTAMP, StandardBasicTypes.INTEGER};
}
#Override
public Object getPropertyValue(Object o, int propertyIndex) {
if (o == null) {
return null;
}
OffsetDateTime offsetDateTime = (OffsetDateTime) o;
switch (propertyIndex) {
case 0:
return Timestamp.valueOf(offsetDateTime.toLocalDateTime());
case 1:
return offsetDateTime.getOffset().getTotalSeconds();
default:
throw new IllegalArgumentException("The propertyIndex (" + propertyIndex
+ ") must be 0 or 1.");
}
}
#Override
public OffsetDateTime nullSafeGet(ResultSet resultSet, String[] names, SessionImplementor session, Object owner)
throws SQLException {
if (resultSet == null) {
return null;
}
Timestamp timestamp = (Timestamp) StandardBasicTypes.TIMESTAMP.nullSafeGet(resultSet, names[0], session, owner);
if (timestamp == null) {
throw new IllegalStateException("The timestamp (" + timestamp + ") for an "
+ OffsetDateTime.class.getSimpleName() + "cannot be null.");
}
LocalDateTime localDateTime = timestamp.toLocalDateTime();
Integer zoneOffsetSeconds = (Integer) StandardBasicTypes.INTEGER.nullSafeGet(resultSet, names[1], session, owner);
if (zoneOffsetSeconds == null) {
throw new IllegalStateException("The zoneOffsetSeconds (" + zoneOffsetSeconds + ") for an "
+ OffsetDateTime.class.getSimpleName() + "cannot be null.");
}
return OffsetDateTime.of(localDateTime, ZoneOffset.ofTotalSeconds(zoneOffsetSeconds));
}
#Override
public void nullSafeSet(PreparedStatement statement, Object value, int parameterIndex, SessionImplementor session)
throws SQLException {
if (value == null) {
statement.setNull(parameterIndex, StandardBasicTypes.TIMESTAMP.sqlType());
statement.setNull(parameterIndex, StandardBasicTypes.INTEGER.sqlType());
return;
}
OffsetDateTime offsetDateTime = (OffsetDateTime) value;
statement.setTimestamp(parameterIndex, Timestamp.valueOf(offsetDateTime.toLocalDateTime()));
statement.setInt(parameterIndex, offsetDateTime.getOffset().getTotalSeconds());
}
// ************************************************************************
// Mutable related methods
// ************************************************************************
#Override
public boolean isMutable() {
return false;
}
#Override
public Object deepCopy(Object value) {
return value; // OffsetDateTime is immutable
}
#Override
public Object replace(Object original, Object target, SessionImplementor session, Object owner) {
return original; // OffsetDateTime is immutable
}
#Override
public void setPropertyValue(Object component, int property, Object value) {
throw new UnsupportedOperationException("A OffsetDateTime is immutable.");
}
// ************************************************************************
// Other methods
// ************************************************************************
#Override
public boolean equals(Object a, Object b) {
if (a == b) {
return true;
} else if (a == null || b == null) {
return false;
}
return a.equals(b);
}
#Override
public int hashCode(Object o) {
if (o == null) {
return 0;
}
return o.hashCode();
}
#Override
public Serializable disassemble(Object value, SessionImplementor session) {
return (Serializable) value;
}
#Override
public Object assemble(Serializable cached, SessionImplementor session, Object owner) {
return cached;
}
}
Now, I want to be able to compare it, so this JPA-QL query works:
#NamedQuery(name = "Shift.myQuery",
query = "select sa from Shift sa" +
" where sa.endDateTime >= :startDateTime" +
" and sa.startDateTime < :endDateTime")
on this model:
#Entity
public class Shift {
#Type(type = "...OffsetDateTimeHibernateType")
#Columns(columns = {#Column(name = "startDateTime"), #Column(name="startDateTimeOffset")})
private OffsetDateTime startDateTime;
#Type(type = "...OffsetDateTimeHibernateType")
#Columns(columns = {#Column(name = "endDateTime"), #Column(name="endDateTimeOffset")})
private OffsetDateTime endDateTime;
...
}
But that fails with:
HHH000177: Error in named query: Shift.myQuery: org.hibernate.hql.internal.ast.QuerySyntaxException: >= operator not supported on composite types. [select sa from org.optaplanner.openshift.employeerostering.shared.shift.Shift sa where sa.endDateTime >= :startDateTime and sa.startDateTime < :endDateTime]
at org.hibernate.hql.internal.ast.QuerySyntaxException.generateQueryException(QuerySyntaxException.java:79)
at org.hibernate.QueryException.wrapWithQueryString(QueryException.java:103)
at org.hibernate.hql.internal.ast.QueryTranslatorImpl.doCompile(QueryTranslatorImpl.java:218)
at org.hibernate.hql.internal.ast.QueryTranslatorImpl.compile(QueryTranslatorImpl.java:142)
at org.hibernate.engine.query.spi.HQLQueryPlan.<init>(HQLQueryPlan.java:115)
at org.hibernate.engine.query.spi.HQLQueryPlan.<init>(HQLQueryPlan.java:76)
at org.hibernate.engine.query.spi.QueryPlanCache.getHQLQueryPlan(QueryPlanCache.java:150)
at org.hibernate.internal.NamedQueryRepository.checkNamedQueries(NamedQueryRepository.java:155)
at org.hibernate.internal.SessionFactoryImpl.checkNamedQueries(SessionFactoryImpl.java:796)
at org.hibernate.internal.SessionFactoryImpl.<init>(SessionFactoryImpl.java:492)
at org.hibernate.boot.internal.SessionFactoryBuilderImpl.build(SessionFactoryBuilderImpl.java:422)
at org.hibernate.jpa.boot.internal.EntityManagerFactoryBuilderImpl.build(EntityManagerFactoryBuilderImpl.java:880)
How can I make my CustomUserType comparable?
Hibernate has no way of knowing how to compare your custom type with multiple columns. You know how the columns relate to each other, but Hibernate doesn't. Without having tested it (can do if I get time later) I think you can rewrite the query to use the property names of the parts, for example:
select sa from Shift sa
where sa.endDateTime.dateTime >= :startDateTimeDateTimePart
and sa.startDateTime.dateTime < :endDateTimeDateTimePart
To make it work with the offset you would need to normalize the value you compare, i.e. add the number of hours represented by the offset to the date. You can do that with a custom function for your database, see 4.6.17.3 in JPA 2.2 (https://jcp.org/aboutJava/communityprocess/mrel/jsr338/index.html). Of course you could also define a custom compare function in the database that takes both parts as input parameters and call it with the function, but personally I would try to stick to the pre-defined functions as much as possible. Adding hours to a timestamp should be covered no matter what database you are using.

Exception when retrieving items modeled by inheritance classes from MongoDB using Datanucleus

me and my team are working on an upgrade of our company's system which as getting kind of forgotten and was running old versions of everything it uses; so developing newer features was becoming a pain with newer and unsupported technologies.
So far we have managed to produce an almost fully working version of the system; but we got stuck at a feature which involves Datanucleus-JDO, MongoDB and inheritance.
We have some models which are tremendously similar (from the code's perspective). In the current in-production version, to apply a change to it usually involves to rewrite the same piece of code in all classes, so we thought that inheritance would make the job easier and better. So we have two interfaces at the top hierarchy level (which as far we know, Datanuclues nor MongoDB doesn't care about them at all); which go like this:
public interface Entity extends Serializable {
String getDate();
double getQty();
void setQty(double qty);
void setDate(String date);
void setKey(Key key);
}
And
public interface HourEntity extends Entity {
String getHour();
}
We use application defined keys, we use this unique class to build different kind of keys. We only want the toString representation of this class to store and retrieve data in Mongo.
public final class Key implements Serializable {
static final long serialVersionUID = -448150158203091507L;
public final String targetClassName;
public final String id;
public final String toString;
public final int hashCode;
public Key() {
targetClassName = null;
id = null;
toString = null;
hashCode = -1;
}
public Key(String str) {
String[] parts = str.split("\\(");
parts[1] = parts[1].replaceAll("\\)", " ");
parts[1] = parts[1].replace("\"", " ");
parts[1] = parts[1].trim();
this.targetClassName = parts[0];
this.id = parts[1];
toString = this.toString();
hashCode = this.hashCode();
}
public Key(String classCollectionName, String id) {
if (StringUtils.isEmpty(classCollectionName)) {
throw new IllegalArgumentException("No collection/class name specified.");
}
if (id == null) {
throw new IllegalArgumentException("ID cannot be null");
}
targetClassName = classCollectionName;
this.id = id;
toString = this.toString();
hashCode = this.hashCode();
}
public String getTargetClassName() {
return targetClassName;
}
public int hashCode() {
if(hashCode != -1) return hashCode;
int prime = 31;
int result = 1;
result = prime * result + (id != null ? id.hashCode() : 0);
result = prime * result + (targetClassName != null ? targetClassName.hashCode() : 0);
return result;
}
public boolean equals(Object object) {
if (object instanceof Key) {
Key key = (Key) object;
if (this == key)
return true;
return targetClassName.equals(key.targetClassName) && Objects.equals(id, key.id);
} else {
return false;
}
}
public String toString() {
if(toString != null) return toString;
StringBuilder buffer = new StringBuilder();
buffer.append(targetClassName);
buffer.append("(");
if (id != null) {
buffer.append((new StringBuilder()).append("\"").append(id)
.append("\"").toString());
} else {
buffer.append("no-id-yet");
}
buffer.append(")");
return buffer.toString();
}
}
This application defined identity is working fine on all other models which does not involve inheritance.
This is one of the actual models that we intend to store in our datastore:
#PersistenceCapable(detachable="true")
#Inheritance(strategy=InheritanceStrategy.COMPLETE_TABLE)
public class Ticket implements Entity {
#PrimaryKey
#Persistent(valueStrategy = IdGeneratorStrategy.UNSPECIFIED, column="_id")
protected Key key;
protected String date;
protected int qty;
public Ticket() {
this.qty = 0;
}
public Key getKey() {
return key;
}
#Override
public void setKey(Key key) {
this.key = key;
}
public double getQty() {
return qty;
}
public void setQty(double qty) {
this.qty = (int) qty;
}
public String getDate() {
return date;
}
public void setDate(String date) {
this.date = date;
}
#Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((key == null) ? 0 : key.hashCode());
return result;
}
#Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Ticket other = (Ticket) obj;
if (key == null) {
if (other.key != null)
return false;
} else if (!key.equals(other.key))
return false;
return true;
}
#Override
public String toString() {
return "Ticket [key=" + key + ", date=" + date + ", qty="
+ qty + "]";
}
}
And this is its subclass (all models which involve this problem just involve one super class and only one children per every super class):
#PersistenceCapable(detachable="true")
#Inheritance(strategy=InheritanceStrategy.COMPLETE_TABLE)
public class HourTicket extends Ticket implements HourEntity {
private String hour;
public HourTicket() {
super();
}
public Key getKey() {
return key;
}
#Override
public void setKey(Key key) {
this.key = key;
}
public String getHour() {
return hour;
}
public void setHour(String hour) {
this.hour = hour;
}
#Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((key == null) ? 0 : key.hashCode());
return result;
}
#Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
HourTicket other = (HourTicket) obj;
if (key == null) {
if (other.key != null)
return false;
} else if (!key.equals(other.key))
return false;
return true;
}
#Override
public String toString() {
return "HourTicket [key=" + key + ", date=" + date
+ ", hour=" + hour + ", qty=" + qty + "]";
}
}
Finally, the persisntance.xml is like this
<?xml version="1.0" encoding="UTF-8" ?>
<persistence xmlns="http://java.sun.com/xml/ns/persistence"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/persistence
http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd" version="2.0">
<!-- JOSAdmin "unit" -->
<persistence-unit name="ourdatastore">
<class>mx.ourdomain.Ticket</class>
<class>mx.ourdomain.HourTicket</class>
<exclude-unlisted-classes/>
</persistence-unit>
</persistence>
And package-mongo.orm
<?xml version="1.0"?>
<!DOCTYPE orm SYSTEM "file:/javax/jdo/orm.dtd">
<orm>
<package name="mx.ourdomain" >
<class name="Ticket" table="Ticket">
<field name="key" primary-key="true" >
<column name="_id" length="100" />
</field >
</class>
<class name="HourTicket" table="HourTicket">
<primary-key >
<column name="_id" target="_id" />
</primary-key>
</class>
</package>
</orm>
So, the problems comes when trying to perform any read or write operations using either the super class or the subclass. This has happened with the same exact results in several (all possible as far we know) scenarios, but the test scenario we are study begins with this call:
Ticket ticket = persistenceManager.getObjectById(Ticket.class, key);
The key is generated with an standard procedure which is used by other models which do store and read successfully; and of course, it is of the previously shown key class.
We have gone as far as debugging the datanucleus tasks beyond this. And we have found that as expected:
The metadata shows that its the super class of others.
Its using application managed keys.
But when trying to get the class name to determine which is the correct Mongo collection to query, datanucleus-mongodb tries to query both classes (Ticket and HourTicket); but then it handles to the mongo driver the key object perse, and then a CodecConfigurationException is thrown since mongo does not know how to work with the key class (when building the query, datanucleus-mongo creates a BasicDBObject which has the structure {_id:key}, which cannot be constructed without the codec because of the key entry. This happens at the MongoDBUtils class in the datanucleus-mongodb project v5.1.0; class MongoDBUtils, method getClassNameForIdentity(Object, AbstractClassMetaData, ExecutionContext, ClassLoaderResolver)).
So, we suppose that we have some configuration missing to tell datanucleus that it should use the toString() form of the key; since the Monogo driver handles String just fine (datanuclues docs actually states that when using custom classes as datastore keys it will use the toString() form of the key; so I'm unsure if this could be a bug).
We have tried to use a KeyTraslator plugin and making the key class a DatastoreId and wrapping in a StringId with no success: the same exception is fired, except when wrapping the Key class in a StringId: the mongo lecture is sucessful but then when trying to build the model object, an ClassCastException is thrown since String cannot be casted into Key, and refactoring the code to use a String key will badly break data already in database; since it has a special format the key class can read and produce.
Is there something we are missing using inheritance with datanucleus JDO w/mongoDB?
I was not putting much attention to the settings around the objectIdClass metadata; since from the docs I got that they were intended for composed keys only. It results that if you define an objectId class with only one attribute; then it behaves as a custom SingleFieldId; which is what we wanted.
I found "funny" the fact that non annotated (or non declared metadata for objectIdClass) classes will work fine and the custom key used will be threated just fine; but once you make any of them a super class, then you are obligated to add the objectIdClass metadata.
Beside annotating the Ticket class (and all other super classes) with objectIdClass, we:
Removed the toString and hashCode attributes from the Key class (#NotPersistent and transient keyword won't make Datanucleus ignore them; so I guess there is no performance improvement for toString() and hashCode() methods on custom keys right now).
Removed all the final qualifiers from the Key class attributes (Datanucleus docs don't say that custom key fields cannot be final; but guess what, they can't be)
Changed the Key key class member from all superclass for String id (as in the key class). We also had to change the implementation of the getters and setters for the id member; using the required string constructor of the key class to build the key when calling the method. Of course, the "key" field declared in the package-mongo.orm was changed to id in the super classes.
And that was it! with those little changes our system is working great; no other changed were required on other persistable classes nor DAOs.

Access entity id inside Hibernate Usertype

I have created my own hibernate UserType implementation class and it works as expected.
I have a requirement now where i need to have access to the value of "id" field of a particular entity record (which is generated via sequence) inside my Hibernate UserType implementation class. Is it possible to achieve this?
Below is my UserType Implementation:
public class SecureStringType implements UserType {
#Override
public int[] sqlTypes() {
return new int[] { Types.VARCHAR };
}
#Override
public Class returnedClass() {
return SecureString.class;
}
#Override
public Object nullSafeGet(ResultSet rs, String[] names, SharedSessionContractImplementor session, Object owner)
throws SQLException {
String encryptedValue = rs.getString(names[0]);
getDataSecurityService().getActualValue(encryptedValue);
SecureString secureString = new SecureString();
secureString.setActualValue(decryptedValue);
return secureString;
}
#Override
public void nullSafeSet(PreparedStatement st, Object value, int index, SharedSessionContractImplementor session)
throws SQLException {
if (value == null) {
st.setNull(index, Types.VARCHAR);
} else {
SecureString actualValue = (SecureString) value;
st.setString(index, getDataSecurityService().encrypt(actualValue ));
}
}
// DeepCopy, disassemble, assemble method implementations
}
I have noticed that SharedSessionContractImplementor has the PersistentContext inside which has list of all entities that are loaded, but I don't have a way to determine for which Entity the UserType is being called.
`session.getPersistenceContext().getEntitiesByKey();` has all the `EntityKey` objects but I need to get the one for which this `UserType` is being called currently.
Any ideas?
I've faced the same problem in my codebase. AFAIK, the only option I could find as of yet is to add an ID field in your SecureString type, set it during #PreUpdate/#PreLoad or other callbacks, or in getters and setters/constructors, or set it using an HibernateInterceptor at whatever lifecycle that suits you. You can then access it in nullSafeSet using value.getId() or something. Its nutty, but it works.

Map some boolean properties as enum Set in Hibernate

I have an entity which has some BIT fields into the database:
editable
needs_review
active
These fields are mapped against boolean fields in its Java class using Hibernate 3.6.9 version. That forces me to write an interface method for each List of entities I want to get:
List<Entity> listEditables();
List<Entity> listReviewNeeded();
List<Entity> listActives();
Or write a general interface method to achieve a combination of them:
List<Entity> listEntities(boolean editables, boolean reviewNeeded, boolean actives);
That second choice looks greater, but if I add another field in the future there will be a need to modify the interface itself (and every line of code coupled to it).
So I decided I can express it as an enumeration Set:
public enum EntityType{
EDITABLE, REVIEW_NEEDED, ACTIVE
}
//That way there's no need to change interface method's signature
List<Entity> listEntities(Set<EntityType> requiredTypes);
It makes sense that being an enumeration match what I want to achieve, the Entity type itself should have its own Set<EntityType>:
public class Entity{
Set<EntityType> entityTypes;
}
However instead of that I have the mapped booleans which logically match that Set. Then my question, is there any way to map Set<EntityType> entityTypes in hibernate based in that BIT fields or do I have to manage that logic myself having them as boolean?
UPDATE
Having them mapped as a Set implies the possibility of querying for a List using an in clause, if not it would imply an extra step for conversion between my controller and model codes.
Set<EntityType> typesSet = Sets.newHashSet(EntityType.EDITABLE, EntityType.REVIEW_NEEDED);
//Obtains a list of every single entity which is EDITABLE or REVIEW_NEEDED
session.createCriteria(Entity.class).addRestriction(Restrictions.in("entityTypes",typeSet)).list();
I think I have a solution for you. What you are interested in is a CompositeUserType.
As an example lets use a InetAddress composite user type I wrote lately to map a 128bit IPv6 Address / IPv4Address object to two 64bit long properties inside a user account entity.
The signupIp:InetAddress is mapped towards two columns (there is no column count limit or alike) using:
#Columns(columns = {#Column(name = "ip_low", nullable = true), #Column(name = "ip_high", nullable = true)})
private InetAddress signupIp;
And the interesting part of the implementation looks like this:
public class InetAddressUserType implements CompositeUserType {
#Override
public String[] getPropertyNames() {
return new String [] {"ipLow", "ipHigh"};
}
#Override
public Type[] getPropertyTypes() {
return new Type [] { LongType.INSTANCE, LongType.INSTANCE};
}
#Override
public Object getPropertyValue(Object component, int property) throws HibernateException {
if(component != null)
return toLong((InetAddress)component)[property];
else
return null;
}
#Override
public void nullSafeSet(PreparedStatement st, Object value, int index,
SessionImplementor session) throws HibernateException, SQLException {
if(value != null) {
long [] longs = toLong((InetAddress)value);
st.setLong(index, longs[0]);
st.setLong(index + 1, longs[1]);
}
else {
st.setNull(index, LongType.INSTANCE.sqlType());
st.setNull(index + 1, LongType.INSTANCE.sqlType());
}
}
#Override
public void setPropertyValue(Object component, int property, Object value)
throws HibernateException {
throw new RuntimeException("This object is immutable");
}
#Override
public Class<?> returnedClass() {
return InetAddress.class;
}
#Override
public boolean equals(Object x, Object y) throws HibernateException {
return x != null ? x.equals(y) : null == y;
}
#Override
public int hashCode(Object x) throws HibernateException {
return x.hashCode();
}
#Override
public Object nullSafeGet(ResultSet rs, String[] names,
SessionImplementor session, Object owner)
throws HibernateException, SQLException {
Long ipLow = rs.getLong(names[0]);
if(!rs.wasNull()) {
Long ipHigh = rs.getLong(names[1]);
try {
return fromLong(new long [] {ipLow, ipHigh});
} catch (UnknownHostException e) {
throw new HibernateException("Failed to get InetAddress: ip = " + ipHigh + " + " + ipLow, e);
}
}
else
return null;
}
#Override
public Object deepCopy(Object value) throws HibernateException {
if(value != null)
try {
return InetAddress.getByAddress(((InetAddress)value).getAddress());
} catch (UnknownHostException e) {
throw new RuntimeException("Impossible Exception: " + e.getMessage(), e);
}
else
return null;
}
#Override
public boolean isMutable() {
return false;
}
...
}
Note that I flexibly switch between Inet4Address and Inet6Address instances depending on the values of ipLow and ipHigh. The composite is marked as immutable and you need to check the documentation and the examples in the Hibernate source code (build in composite user types).
In a similar way you can map your meaningful bit properties. You can query those bits by using a single Restriction.eq refering to your EnumType. You can use the equals method to check the properties object. And if you need to refer to a special mapped bit you can use the dot notation like in signupIp.ipLow to refer to the ipLow property/column.
I guess this is what you are looking for.
Update:
In the end it boils down to define the right order of your properties. Hibernate will always use integer index values to access each property:
//immutable for simplicity
class Status {
private final boolean editable;
private final boolean needsReview;
private final boolean active;
//... constructor + isEditable etc..
}
In your StatusCompositeType class:
public String[] getPropertyNames() {
return new String [] {"editable", "needsReview", "active"};
}
public Type[] getPropertyTypes() {
return new Type [] { BooleanType.INSTANCE, LongType.INSTANCE};
}
public Object getPropertyValue(Object component, int property) throws HibernateException {
if(component != null) {
Status status = (Status)component;
switch(property) {
case 1: return status.isEditable();
case 2: return status.isReviewNeeded();
case 3: return status.isActive();
default: throw new IllegalArgumentException();
}
}
else
return null; //all columns can be set to null if you allow a entity to have a null status.
}
public void nullSafeSet(PreparedStatement st, Object value, int index,
SessionImplementor session) throws HibernateException, SQLException {
if(value != null) {
Status status = (Status)value;
st.setBoolean(index, status.isEditable());
st.setBoolean(index + 1, status.isReviewNeeded());
st.setBoolean(index + 2, status.isActive());
}
else {
st.setNull(index, BooleanType.INSTANCE.sqlType());
st.setNull(index + 1, BooleanType.INSTANCE.sqlType());
st.setNull(index + 2, BooleanType.INSTANCE.sqlType());
}
}
public Object nullSafeGet(ResultSet rs, String[] names,
SessionImplementor session, Object owner)
throws HibernateException, SQLException {
Boolean isEditable = rs.getBoolean(names[0]);
if(!rs.wasNull()) {
Boolean isReviewNeeded = rs.getBoolean(names[1]);
Boolean isActive = rs.getBoolean(names[2]);
return new Status(isEditable, isReviewNeeded, isActive);
}
else
return null;
}
The rest is straight forward. Remember to implement equals and hashcode for the user type and add the type to the configuration before you create your sessionFactory.
Once you have everything in place you can create a criteria search and use:
//search for any elements that have a status of editable, no reviewNeeded and is not active (true false false).
criteria.add(Restrictions.eq("status", new Status(true, false, false));
Now your listEntities method may become either: listEntities(Status status) or listEntities(boolean editable, boolean reviewNeeded, boolean isActive).
If you need additional information just check the CompositeType and BasicType implementations Hibernate provides within its own sourcecode (look for implementors of CompositeType and BasicType). Understanding those helps alot to use and learn this intermediate level knowledge of Hibernate.
After some brainstorming, I've gone to a workaround which I consider the second best one being imposible to map an enum for the booleans in Hibernate. This is how I have my Entity class looks now:
public class Entity{
private boolean editable;
private boolean needsReview;
private boolean active;
//getters and setters
}
My listing method is implemented as this:
public List<Entity> listEntities(Set<EntityType> requiredTypes){
Criteria cri = session.createCriteria(Entity.class);
if (requiredTypes.contains(EntityType.EDITABLE)){
cri.addRestriction(Restrictions.eq("editable",true));
}
if (requiredTypes.contains(EntityType.NEEDS_REVIEW)){
cri.addRestriction(Restrictions.eq("needsReview",true));
}
if (requiredTypes.contains(EntityType.ACTIVE)){
cri.addRestriction(Restrictions.eq("active",true));
}
return cri.list();
}
Not bad, but don't know if it's the only way to go with that!
I don't think hibernate provides a way to manage the mappings the way you're describing. You can create your own UserType (https://community.jboss.org/wiki/Java5EnumUserType) but every time you add a new enum value you will have to change the logic in the UserType to map the new field as well.
The alternative will be to convert this into a one to many relationship. Your point is basically that if you want to add more fields you will have to change the signature of listEntities but also you will have to modify your table.
So, instead you can create a table that will contain your entity types and have a #OneToMany` relationship to it from your entity. For example:
Define your flags as required:
public enum Flags {
EDITABLE, REVIEW_NEEDED, ACTIVE
}
Create a one-to-many relationship to EntityType:
#Entity
#Table( name="entity" )
public class Entity implements Serializable {
#OneToMany(mappedBy = "entity")
public Set<EntityType> getEntityTypes() {
return entityTypes;
}
And a many-to-one to Entity:
#Entity
#Table( name="entityType" )
public class EntityType implements Serializable {
#Id
private Integer id;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "ENTITY_ID")
private Entity entity;
#Enumerated(EnumType.STRING)
private Flag entityType;
...
}
PD: Please note the code is just an example and is not complete or tested.

Hibernate Entities from Multiple Databases

Our data model is separated into schemas on two databases. The schemas are used in isolation except for a few single-key relationships that bridge between the two. There are no write transactions that will span both databases.
Similar to this question Doing a join over 2 tables in different databases using Hibernate, we want to use Hibernate to handle joining the entities. We cannot use the database solution (Federated views on DB2).
We have set up Hibernate with two separate database configurations (Doctor and Patient), which works perfectly when using DAOs to explicitly access a particular session.
We want to use Hibernate to automatically retrieve the entity when we call DoctorBO.getExam().getPatient() Where examination contains an id pointing to the Patient table on the other database.
One way I've tried doing this is using a custom UserType:
public class DistributedUserType implements UserType, ParameterizedType
{
public static final String CLASS = "CLASS";
public static final String SESSION = "SESSION";
private Class<? extends DistributedEntity> returnedClass;
private String session;
/** {#inheritDoc} */
#Override
public int[] sqlTypes()
{
// The column will only be the id
return new int[] { java.sql.Types.BIGINT };
}
/** {#inheritDoc} */
#Override
public Class<? extends DistributedEntity> returnedClass()
{
// Set by typedef parameter
return returnedClass;
}
/** {#inheritDoc} */
#Override
public boolean equals(Object x, Object y) throws HibernateException
{
if (x == y)
{
return true;
}
if ((x == null) || (y == null))
{
return false;
}
Long xId = ((DistributedEntity) x).getId();
Long yId = ((DistributedEntity) y).getId();
if (xId.equals(yId))
{
return true;
}
else
{
return false;
}
}
/** {#inheritDoc} */
#Override
public int hashCode(Object x) throws HibernateException
{
assert (x != null);
return x.hashCode();
}
/** {#inheritDoc} */
#Override
public Object nullSafeGet(ResultSet rs, String[] names, Object owner) throws HibernateException, SQLException
{
Long id = rs.getLong(names[0]);
return HibernateUtils.getSession(session).get(returnedClass, id);
}
/** {#inheritDoc} */
#Override
public void nullSafeSet(PreparedStatement st, Object value, int index) throws HibernateException, SQLException
{
DistributedEntity de = (DistributedEntity) value;
st.setLong(index, de.getId());
}
/** {#inheritDoc} */
#Override
public Object deepCopy(Object value) throws HibernateException
{
return value;
}
/** {#inheritDoc} */
#Override
public boolean isMutable()
{
return false;
}
/** {#inheritDoc} */
#Override
public Serializable disassemble(Object value) throws HibernateException
{
return (Serializable) value;
}
/** {#inheritDoc} */
#Override
public Object assemble(Serializable cached, Object owner) throws HibernateException
{
return cached;
}
/** {#inheritDoc} */
#Override
public Object replace(Object original, Object target, Object owner) throws HibernateException
{
return original;
}
/** {#inheritDoc} */
#Override
public void setParameterValues(Properties parameters)
{
String clazz = (String) parameters.get(CLASS);
try
{
returnedClass = ReflectHelper.classForName(clazz);
}
catch (ClassNotFoundException e)
{
throw new IllegalArgumentException("Class: " + clazz + " is not a known class type.");
}
session = (String) parameters.get(SESSION);
}
}
Which would then be used:
#TypeDef(name = "testUserType", typeClass = DistributedUserType.class, parameters = {
#Parameter(name = DistributedUserType.CLASS, value = PatientBO.CLASSNAME),
#Parameter(name = DistributedUserType.SESSION, value = HibernateUtils.PATIENT_SESS) })
#Type(type = "testUserType")
#Column(name = "PATIENT_ID")
private PatientBO patient;
The UserType works - the data is loaded correctly with only the Id of the field persisted to the database. I have tested very simple examples of doctor.getExam().getPatient() and doctor.getExam().setPatient() and both seem to work great, however I think this is a terrible hack and I do not have adequate knowledge of Hibernate to know if this is safe to use.
Is there a better way to achieve what we want? Is the way I've described here adequate, or will it cause difficulties in the future?
I don't think it's a good idea. You're trying to make "as if" everything was in a single database, whereas it's not the case. And you make "as if" there was a real toOne association between an exam and a patient, although it's not a real association.
Although you are conscious of this fact, other or future developers won't necessarily be, and will wonder why it's not possible to make a query such as
select e from Exam e left join fetch e.patient
or
select e from Exam e where e.patient.name like 'Smith%'
In short, your pseudo-association only fulfills a very small part of the contract a regular association offers, and this will, IMO, cause more confusion than comfort.
Nothing stops you from having a utility method like
Patient getExamPatient(Exam e)
that does the same thing, but makes it clear that there is no real asociation between both entities.

Categories

Resources