如何将 grails 域 class 属性配置为存储为 (postgres 9.4) jsonb?

How do I configure a grails domain class attribute to be stored as (postgres 9.4) jsonb?

我试过像这样配置域 class:

class Test {

    String data

    static constraints = {
    }

    static mapping = {
        data type: 'jsonb'
    }
}

这会引发异常(最终原因是Invocation of init method failed; nested exception is org.hibernate.MappingException: Could not determine type for: jsonb, at table: test, for columns: [org.hibernate.mapping.Column(data)])。

我也尝试了 column: 'data', sqlType: 'jsonb',它创建了一个名为 data.

text

如何正确地告诉 grails 使用 jsonb 作为 sql 列类型?有可能吗?

(postgresql jdbc 驱动程序在版本 9.4-1200 中使用。jdbc4 with hibernate 4。)

您可以使用 Grails Postgresql Extensions 插件在您的域中使用一些 Postgresql 本机类型 类。

目前插件支持Json但不支持Jsonb类型。您可以在 plugin documentation

中获得有关 json 支持的更多信息

免责声明:我是该插件的开发者之一。

要配置域以将 jsonb 类型映射到 String,您可以:

  1. 声明你自己的org.hibernate.usertype.UserType。添加到 src/java:

    public class JSONBType implements UserType {
    
        @Override
        public int[] sqlTypes() {
            return new int[] { Types.OTHER };
        }
    
        @SuppressWarnings("rawtypes")
        @Override
        public Class returnedClass() {
            return String.class;
        }
    
        @Override
        public boolean equals(Object x, Object y) throws HibernateException {
            return (x != null) && x.equals(y);
        }
    
        @Override
        public int hashCode(Object x) throws HibernateException {
            return x.hashCode();
        }
    
        @Override
        public Object nullSafeGet(ResultSet rs, String[] names, SessionImplementor sessionImplementor, Object owner)
            throws HibernateException, SQLException {
            return rs.getString(names[0]);
        }
    
        @Override
        public void nullSafeSet(PreparedStatement st, Object value, int index, SessionImplementor sessionImplementor)
            throws HibernateException, SQLException {
            st.setObject(index, value, (value == null) ? Types.NULL : Types.OTHER);
        }
    
        @Override
        public Object deepCopy(Object value) throws HibernateException {
            if (value == null) return null;
            return new String((String)value);
        }
    
        @Override
        public boolean isMutable() {
            return false;
        }
    
        @Override
        public Serializable disassemble(Object value) throws HibernateException {
            return (Serializable)value;
        }
    
        @Override
        public Object assemble(Serializable cached, Object owner)
            throws HibernateException {
            return cached;
        }
    
        @Override
        public Object replace(Object original, Object target, Object owner)
            throws HibernateException {
            return deepCopy(original);
        }
    }
    
  2. 之后你可以简单地在域中声明映射:

    static mapping = {
        data type: "your.package.JSONBType", sqlType: "jsonb"
    }
    

您也可以不将 jsonb 映射到 String,而是直接映射到 JSONObject 或您现有的 class 或界面。在这种情况下,GORM 将负责 serializing/deserializing json 并且您不再需要在应用程序中明确地这样做。 Here is an example of such UserType implementation.

尽管我很晚才回答这个问题,但我设法通过一种非常简单的方式实现了这一点,而且工作非常顺利-

我创建了一个实现 UserType:

的自定义 Hibernate 类型
package com.wizpanda.hibernate

import groovy.transform.CompileStatic
import org.grails.web.json.JSONObject
import org.hibernate.HibernateException
import org.hibernate.engine.spi.SessionImplementor
import org.hibernate.usertype.UserType

import java.sql.PreparedStatement
import java.sql.ResultSet
import java.sql.SQLException
import java.sql.Types

/**
 * An implementation of {@link org.grails.web.json.JSONObject} column using Hibernate custom types.
 * https://docs.jboss.org/hibernate/orm/current/userguide/html_single/Hibernate_User_Guide.html#_custom_type
 * https://docs.jboss.org/hibernate/orm/current/javadocs/org/hibernate/usertype/UserType.html
 *
 * @author Shashank Agrawal
 */
@CompileStatic
class JSONObjectFooType implements UserType {

    @Override
    int[] sqlTypes() {
        return [Types.OTHER] as int[]
    }

    //@SuppressWarnings("rawtypes")
    @Override
    Class returnedClass() {
        return JSONObject.class
    }

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

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

    @Override
    Object nullSafeGet(ResultSet rs, String[] names, SessionImplementor session, Object owner) throws HibernateException, SQLException {
        String value = rs.getString(names[0])
        if (!value) {
            return null
        }

        return new JSONObject(value)
    }

    @Override
    void nullSafeSet(PreparedStatement st, Object value, int index, SessionImplementor session) throws HibernateException, SQLException {
        String valueToPersist

        if (value) {
            if (value instanceof JSONObject) {
                valueToPersist = value.toString()
            } else if (value instanceof String) {
                valueToPersist = new JSONObject(value).toString(0)
            } else {
                throw new HibernateException("Unknown type received for JSONObject based column")
            }
        }

        st.setObject(index, valueToPersist, Types.OTHER)
    }

    @Override
    Object deepCopy(Object value) throws HibernateException {
        if (!value) {
            return null
        }
        if (value instanceof JSONObject) {
            return new JSONObject(value.toString(0))
        }

        return value
    }

    @Override
    boolean isMutable() {
        return false
    }

    @Override
    Serializable disassemble(Object value) throws HibernateException {
        if (value instanceof JSONObject) {
            return value?.toString(0)
        }

        return value?.toString()
    }

    @Override
    Object assemble(Serializable cached, Object owner) throws HibernateException {
        if (!cached) {
            return null
        }

        return new JSONObject(cached.toString())
    }

    @Override
    Object replace(Object original, Object target, Object owner) throws HibernateException {
        return deepCopy(original)
    }
}

我正在使用 org.grails.web.json.JSONObject 因为这是 Grails 的内部版本,您可以使用 org.json.JSONObject 或 Groovy json 等其他内容并替换上面出现的内容。

现在,只需在您的域中使用它 class-

class User {

    String email
    String name
    JSONObject settings

    static mapping = {
        settings type: JSONObjectFooType, sqlType: "text"
    }
}

合十礼!

游戏有点晚了,但对于后代来说,这里是我对@jasp 提供的解决方案的版本。不同之处在于此解决方案会将 Map 对象作为 JSON 格式持久保存到文本列。它使用包含 Jackson 库的 grails。

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.hibernate.HibernateException;
import org.hibernate.engine.spi.SharedSessionContractImplementor;
import org.hibernate.usertype.UserType;

import java.io.IOException;
import java.io.Serializable;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Types;
import java.util.Map;

public class JSONStringType implements UserType {
    private static final ObjectMapper _mapper = new ObjectMapper();

    @Override
    public int[] sqlTypes() {
        return new int[] { Types.VARCHAR };
    }

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

    @Override
    public boolean equals(Object x, Object y) throws HibernateException {
        return (x != null) && x.equals(y);
    }

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

    @Override
    public Object nullSafeGet(ResultSet rs, String[] names, SharedSessionContractImplementor session, Object owner) throws HibernateException, SQLException {
        try {
            String val = rs.getString(names[0]);
            return _mapper.readValue(val, Map.class);
        } catch (IOException e) {
            throw new HibernateException(e);
        }
    }

    @Override
    public void nullSafeSet(PreparedStatement st, Object value, int index, SharedSessionContractImplementor session) throws HibernateException, SQLException {
        try {
            String val;
            if (value == null)
                val = "{}";
            else if (value instanceof String)
                val = (String)value;
            else
                val = _mapper.writeValueAsString(value);
            st.setObject(index, val, (value == null) ? Types.NULL : Types.VARCHAR);
        } catch (JsonProcessingException e) {
            throw new HibernateException(e);
        }
    }

    @Override
    public Object deepCopy(Object value) throws HibernateException {
        if (value == null) return null;
        try {
            String val = _mapper.writeValueAsString(value);
            return val;
        } catch (JsonProcessingException e) {
            throw new HibernateException(e);
        }
    }

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

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

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

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

用法:

import your.package.JSONStringType

class Book {
    String name
    String isbn
    Map attributes = [:]

    static constraints = {
    }

    static mapping = {
        attributes type: JSONStringType, sqlType: 'nvarchar(4000)'
    }
}

更改 sqlType 以匹配您的数据库列类型。对于 SQL 服务器,nvarchar(4000) 用于高效 JSON 文档查询或 nvarchar(MAX) 用于大型 JSON 文档存储。