5

I am trying to implement the Hstore-Datatype and use it in JPA Entities. However, when I try to persist some testdata, I get several errors.

For implementing the Hstore-Datatype I used the following tutorial: Storing Sets of Key/value Pairs in a Single Db Column With Hibernate Using PostgreSQL Hstore Type

This is the code, which I have right now in my application:

A helper for converting a Map to a String conforming to hstore syntax and vice versa:

public class HstoreHelper {

    private static final String K_V_SEPARATOR = "=>";

    public static String toString(Map<String, String> m) {
        if (m.isEmpty()) {
            return "";
        }
        StringBuilder sb = new StringBuilder();
        int n = m.size();
        for (String key : m.keySet()) {
            sb.append("\"" + key + "\"" + K_V_SEPARATOR + "\"" + m.get(key) + "\"");
            if (n > 1) {
                sb.append(", ");
                n--;
            }
        }
        return sb.toString();
    }

    public static Map<String, String> toMap(String s) {
        Map<String, String> m = new HashMap<String, String>();
        if (s.isEmpty()) {
            return m;
        }
        String[] tokens = s.split(", ");
        for (String token : tokens) {
            String[] kv = token.split(K_V_SEPARATOR);
            String k = kv[0];
            k = k.trim().substring(1, k.length() - 1);
            String v = kv[1];
            v = v.trim().substring(1, v.length() - 1);
            m.put(k, v);
        }
        return m;
    }
}

A UserType implementation:

public class HstoreUserType implements UserType {

      public Object assemble(Serializable cached, Object owner)
              throws HibernateException {
          return cached;
      }

      public Object deepCopy(Object o) throws HibernateException {
          Map m = (Map) o;
          return new HashMap(m);
      }

      public Serializable disassemble(Object o) throws HibernateException {
          return (Serializable) o;
      }

      public boolean equals(Object o1, Object o2) throws HibernateException {
          Map m1 = (Map) o1;
          Map m2 = (Map) o2;
          return m1.equals(m2);
      }

      public int hashCode(Object o) throws HibernateException {
          return o.hashCode();
      }

      public boolean isMutable() {
          return true;
      }

      public Object nullSafeGet(ResultSet rs, String[] strings, Object o)
              throws HibernateException, SQLException {
          String col = strings[0];
          String val = rs.getString(col);
          return HstoreHelper.toMap(val);
      }

      public void nullSafeSet(PreparedStatement ps, Object obj, int i)
              throws HibernateException, SQLException {
          String s = HstoreHelper.toString((Map) obj);
          ps.setObject(i, s, Types.OTHER);
      }

      public Object replace(Object original, Object target, Object owner)
              throws HibernateException {
          return original;
      }

      public Class returnedClass() {
          return Map.class;
      }

      public int[] sqlTypes() {
          return new int[] { Types.INTEGER };
      }
    }

And using it in an entity bean:

@Entity
@TypeDef(name = "hstore", typeClass = HstoreUserType.class)
@XmlRootElement
@Table(name = "address")
public class Address {
    .
    .
    .
    @Type(type = "hstore")
    @Column(columnDefinition = "hstore")
    private Map<String, String> hs = new HashMap<String, String>();
    .
    .
    .
}

I tried to publish the application with the instances of address, but these errors appear:

14:40:47,906 INFO  [stdout] (MSC service thread 1-1) Hibernate: insert into address (housenumber, location, street, zipcode, address_id) values (?, ?, ?, ?, ?)

14:40:47,921 WARN  [com.arjuna.ats.arjuna] (MSC service thread 1-1) ARJUNA012125: TwoPhaseCoordinator.beforeCompletion - failed for SynchronizationImple< 0:ffff0a49215c:-6aa8b5cf:53b3d0e3:133, org.hibernate.engine.transaction.synchronization.internal.RegisteredSynchronization@628b66 >: java.lang.AbstractMethodError
at org.hibernate.type.CustomType.nullSafeSet(CustomType.java:155) [hibernate-core-4.0.1.Final.jar:4.0.1.Final]

14:40:48,062 INFO  [org.hibernate.engine.jdbc.batch.internal.AbstractBatchImpl] (MSC service thread 1-1) HHH000010: On release of batch it still contained JDBC statements
14:40:48,093 ERROR [org.jboss.msc.service.fail] (MSC service thread 1-1) MSC00001: Failed to start service jboss.deployment.unit."ductbased.war".component.TestDataGenerator.START: org.jboss.msc.service.StartException in service jboss.deployment.unit."ductbased.war".component.TestDataGenerator.START: Failed to start service
at org.jboss.msc.service.ServiceControllerImpl$StartTask.run(ServiceControllerImpl.java:1767) [jboss-msc-1.0.2.GA.jar:1.0.2.GA]

14:40:49,265 ERROR [org.jboss.as.server.deployment.scanner] (DeploymentScanner-threads - 2) {"JBAS014653: Composite operation failed and was rolled back. Steps that failed:" => {"Operation step-2" => {"JBAS014671: Failed services" => {"jboss.deployment.unit.\"ductbased.war\".component.TestDataGenerator.START" => "org.jboss.msc.service.StartException in service jboss.deployment.unit.\"ductbased.war\".component.TestDataGenerator.START: Failed to start service"}}}}

Could someone please tell me, why the error appears? The author of the tutorial claims, that the code works for him, so I'm left alone a bit :D And it is also the first time, I implement an own datatype. Thank you in advance!

4
  • Could you create a project on github or something? Hard to debug like this. Commented Nov 26, 2014 at 16:30
  • Did you ever get an answer to this? We're trying to figure out what Types.X to use here. Commented Dec 31, 2014 at 17:00
  • Unfortunately I didn't and I no longer have access to the source-files. I only had a little time to experiment with this. Would be great, if someone implements it in the right way and shares it, though. Commented Jan 4, 2015 at 23:40
  • See my answer to a similar question here: stackoverflow.com/a/38561178/466738 Commented Jul 25, 2016 at 6:41

2 Answers 2

7

For those landing up here in 2020, hibernate-types now supports it out of the box. All Credits go to Vlad Mihalcea. Copying parts of the code to preserve the same.

https://vladmihalcea.com/map-postgresql-hstore-jpa-entity-property-hibernate/

<dependency>
    <groupId>com.vladmihalcea</groupId>
    <artifactId>hibernate-types-52</artifactId>
    <version>${hibernate-types.version}</version>
</dependency>

In your DB

CREATE EXTENSION IF NOT EXISTS hstore;
ALTER Table <Table_Name> ADD COLUMN <column_name> hstore;

In your POJO

@Entity(name = "Book")
@Table(name = "book")
@TypeDef(name = "hstore", typeClass = PostgreSQLHStoreType.class)
public class Book {
 
    @Id
    @GeneratedValue
    private Long id;
 
    @NaturalId
    @Column(length = 15)
    private String isbn;
 
    @Type(type = "hstore")
    @Column(columnDefinition = "hstore")
    private Map<String, String> properties = new HashMap<>();
 
    //Getters and setters omitted for brevity
}

Sample Entry

Book book = new Book();
 
book.setIsbn("978-9730228236");
book.getProperties().put("title", "High-Performance Java Persistence");
book.getProperties().put("author", "Vlad Mihalcea");
book.getProperties().put("publisher", "Amazon");
book.getProperties().put("price", "$44.95");
 
entityManager.persist(book);
Sign up to request clarification or add additional context in comments.

Comments

0

Here is an example how to use HStore with Hibernate.

First write an user type:

/**
 * Custom Hibernate {@link UserType} used to convert between a {@link Map}
 * and PostgreSQL {@code hstore} data type.
 */
public class HStoreType implements UserType {

    /**
     * PostgreSQL {@code hstore} field separator token.
     */
    private static final String HSTORE_SEPARATOR_TOKEN = "=>";

    /**
     * {@link Pattern} used to find and split {@code hstore} entries.
     */
    private static final Pattern HSTORE_ENTRY_PATTERN = Pattern.compile(
        String.format("\"(.*)\"%s\"(.*)\"", HSTORE_SEPARATOR_TOKEN)
    );
    
    public static final int SQL_TYPE = Types.OTHER;

    @Override
    public int[] sqlTypes() {
        return new int[] { SQL_TYPE };
    }

    @SuppressWarnings("rawtypes")
    @Override
    public Class returnedClass() {
        return Map.class;
    }

    @Override
    public boolean equals(final Object x, final Object y) throws HibernateException {
        return x.equals(y);
    }

    @Override
    public int hashCode(final Object x) throws HibernateException {
        return x.hashCode();
    }

    @Override
    public Object nullSafeGet(final ResultSet rs, final String[] names,
            final SharedSessionContractImplementor session, final Object owner)
            throws HibernateException, SQLException {
        return convertToEntityAttribute(rs.getString(names[0]));
    }

    @SuppressWarnings("unchecked")
    @Override
    public void nullSafeSet(final PreparedStatement st, final Object value, final int index,
            final SharedSessionContractImplementor session) throws HibernateException, SQLException {
        st.setObject(index, convertToDatabaseColumn((Map<String,Object>)value), SQL_TYPE);

    }

    @SuppressWarnings("unchecked")
    @Override
    public Object deepCopy(final Object value) throws HibernateException {
        return new HashMap<String,Object>(((Map<String,Object>)value));
    }

    @Override
    public boolean isMutable() {
        return true;
    }

    @Override
    public Serializable disassemble(final Object value) throws HibernateException {
        return (Serializable) value;
    }

    @Override
    public Object assemble(final Serializable cached, final Object owner)
            throws HibernateException {
        return cached;
    }

    @Override
    public Object replace(final Object original, final Object target, final Object owner)
            throws HibernateException {
        return original;
    }


    private String convertToDatabaseColumn(final Map<String, Object> attribute) {
        final StringBuilder builder = new StringBuilder();
        for (final Map.Entry<String, Object> entry : attribute.entrySet()) {
            if(builder.length() > 1) {
                builder.append(", ");
            }
            builder.append("\"");
            builder.append(entry.getKey());
            builder.append("\"");
            builder.append(HSTORE_SEPARATOR_TOKEN);
            builder.append("\"");
            builder.append(entry.getValue().toString());
            builder.append("\"");
        }
        return builder.toString();
    }

    private Map<String, Object> convertToEntityAttribute(final String dbData) {
        final Map<String, Object> data = new HashMap<String, Object>();
        final StringTokenizer tokenizer = new StringTokenizer(dbData, ",");

        while(tokenizer.hasMoreTokens()) {
            final Matcher matcher = HSTORE_ENTRY_PATTERN.matcher(tokenizer.nextToken().trim());
            if(matcher.find()) {
                data.put(matcher.group(1), matcher.group(2));
            }
        }

        return data;
    }
}

Secondly write an SQL function adapter:

public class HStoreValueFunction implements SQLFunction {

    @Override
    public boolean hasArguments() {
        return true;
    }

    @Override
    public boolean hasParenthesesIfNoArguments() {
        return false;
    }

    @Override
    public Type getReturnType(Type type, Mapping mpng) throws QueryException {
        return new StringType();
    }

    @Override
    public String render(Type type, List args, SessionFactoryImplementor sfi) throws QueryException {
        if (args.size() < 2) {
            throw new IllegalArgumentException("2 arguments required");
        }
        String field = (String) args.get(0);
        String key = (String) args.get(1);
        return field + " -> " + key;
    }
}

and register it in a custom dialect:

public class ExpandedPostgresDialect extends PostgreSQL95Dialect {

    public ExpandedPostgresDialect() {
        super();
        registerFunction("hstoreValue", new HStoreValueFunction());
    }
}

As a result you will be able to use it in entities like:

@Embeddable
public class TranslatableField {
    
    private String value;
    
    @Type(type = "it.walczak.examples.in18jpa.concepts.b.hibernate.HStoreType")
    @Column(columnDefinition = "hstore")
    private Map<String, String> translationByLanguage = new HashMap<>();

and in JPQL queries like:

select p from Product p
where lower(p.details.name.value) like concat('%', lower(?1), '%')
or lower(hstoreValue(p.details.name.translationsByLanguage, ?2))
like concat('%', lower(?1), '%')

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.