Implementing PostgreSQL Hstore for JPA/Hibernate
Asked Answered
B

2

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!

Bummalo answered 2/7, 2014 at 13:6 Comment(4)
Could you create a project on github or something? Hard to debug like this.Tyrannous
Did you ever get an answer to this? We're trying to figure out what Types.X to use here.Overly
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.Bummalo
See my answer to a similar question here: https://mcmap.net/q/1339168/-is-there-a-elegant-and-common-way-for-converting-postgresql-hstore-to-jpa-2-1-datatypeKitchenware
F
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);
Froghopper answered 2/12, 2020 at 12:7 Comment(0)
O
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), '%')
Occur answered 31/7, 2023 at 15:45 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.