web-dev-qa-db-fra.com

Comment mapper une colonne JSON de carte à Java Object avec JPA

Nous avons une grande table avec beaucoup de colonnes. Après avoir migré vers MySQL Cluster, la table ne peut pas être créée à cause de:

ERREUR 1118 (42000): taille de ligne trop grande. La taille de ligne maximale pour le type de table utilisé, sans compter les BLOB, est de 14 000. Cela inclut les frais généraux de stockage, consultez le manuel. Vous devez changer certaines colonnes en TEXT ou BLOBs

Par exemple:

@Entity @Table (name = "appconfigs", schema = "myproject")
public class AppConfig implements Serializable
{
    @Id @Column (name = "id", nullable = false)
    @GeneratedValue (strategy = GenerationType.IDENTITY)
    private int id;

    @OneToOne @JoinColumn (name = "app_id")
    private App app;

    @Column(name = "param_a")
    private ParamA parama;

    @Column(name = "param_b")
    private ParamB paramb;
}

C'est un tableau pour stocker les paramètres de configuration. Je pensais que nous pouvons combiner certaines colonnes en une seule et la stocker en tant qu'objet JSON et la convertir en un objet Java.

Par exemple:

@Entity @Table (name = "appconfigs", schema = "myproject")
public class AppConfig implements Serializable
{
    @Id @Column (name = "id", nullable = false)
    @GeneratedValue (strategy = GenerationType.IDENTITY)
    private int id;

    @OneToOne @JoinColumn (name = "app_id")
    private App app;

    @Column(name = "params")
    //How to specify that this should be mapped to JSON object?
    private Params params;
}

Où nous avons défini:

public class Params implements Serializable
{
    private ParamA parama;
    private ParamB paramb;
}

En utilisant cela, nous pouvons combiner toutes les colonnes en une seule et créer notre table. Ou nous pouvons diviser la table entière en plusieurs tables. Personnellement, je préfère la première solution.

Quoi qu'il en soit, ma question est de savoir comment mapper la colonne Params qui est du texte et contient une chaîne JSON d'un objet Java?

16
Rad

Vous pouvez utiliser un convertisseur JPA pour mapper votre entité à la base de données. Ajoutez simplement une annotation similaire à celle-ci dans votre champ params:

@Convert(converter = JpaConverterJson.class)

puis créez la classe d'une manière similaire (cela convertit un objet générique, vous voudrez peut-être le spécialiser):

@Converter(autoApply = true)
public class JpaConverterJson implements AttributeConverter<Object, String> {

  private final static ObjectMapper objectMapper = new ObjectMapper();

  @Override
  public String convertToDatabaseColumn(Object meta) {
    try {
      return objectMapper.writeValueAsString(meta);
    } catch (JsonProcessingException ex) {
      return null;
      // or throw an error
    }
  }

  @Override
  public Object convertToEntityAttribute(String dbData) {
    try {
      return objectMapper.readValue(dbData, Object.class);
    } catch (IOException ex) {
      // logger.error("Unexpected IOEx decoding json from database: " + dbData);
      return null;
    }
  }

}

C'est tout: vous pouvez utiliser cette classe pour sérialiser n'importe quel objet en json dans la table.

41

Comme je l'ai expliqué dans cet article , le JPA AttributeConverter est bien trop limité pour mapper les types d'objets JSON, surtout si vous souhaitez les enregistrer en tant que binaire JSON.

Vous n'avez pas besoin de créer tous ces types manuellement, vous pouvez simplement les obtenir via Maven Central en utilisant la dépendance suivante:

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

Pour plus d'informations, consultez le projet open-source hibernate-types .

Maintenant, pour expliquer comment tout cela fonctionne.

J'ai écrit n article sur la façon dont vous pouvez mapper des objets JSON sur PostgreSQL et MySQL.

Pour PostgreSQL, vous devez envoyer l'objet JSON sous une forme binaire:

public class JsonBinaryType
    extends AbstractSingleColumnStandardBasicType<Object> 
    implements DynamicParameterizedType {

    public JsonBinaryType() {
        super( 
            JsonBinarySqlTypeDescriptor.INSTANCE, 
            new JsonTypeDescriptor()
        );
    }

    public String getName() {
        return "jsonb";
    }

    @Override
    public void setParameterValues(Properties parameters) {
        ((JsonTypeDescriptor) getJavaTypeDescriptor())
            .setParameterValues(parameters);
    }

}

Le JsonBinarySqlTypeDescriptor ressemble à ceci:

public class JsonBinarySqlTypeDescriptor
    extends AbstractJsonSqlTypeDescriptor {

    public static final JsonBinarySqlTypeDescriptor INSTANCE = 
        new JsonBinarySqlTypeDescriptor();

    @Override
    public <X> ValueBinder<X> getBinder(
        final JavaTypeDescriptor<X> javaTypeDescriptor) {
        return new BasicBinder<X>(javaTypeDescriptor, this) {
            @Override
            protected void doBind(
                PreparedStatement st, 
                X value, 
                int index, 
                WrapperOptions options) throws SQLException {
                st.setObject(index, 
                    javaTypeDescriptor.unwrap(
                        value, JsonNode.class, options), getSqlType()
                );
            }

            @Override
            protected void doBind(
                CallableStatement st, 
                X value, 
                String name, 
                WrapperOptions options)
                    throws SQLException {
                st.setObject(name, 
                    javaTypeDescriptor.unwrap(
                        value, JsonNode.class, options), getSqlType()
                );
            }
        };
    }
}

et le JsonTypeDescriptor comme ceci:

public class JsonTypeDescriptor
        extends AbstractTypeDescriptor<Object> 
        implements DynamicParameterizedType {

    private Class<?> jsonObjectClass;

    @Override
    public void setParameterValues(Properties parameters) {
        jsonObjectClass = ( (ParameterType) parameters.get( PARAMETER_TYPE ) )
            .getReturnedClass();

    }

    public JsonTypeDescriptor() {
        super( Object.class, new MutableMutabilityPlan<Object>() {
            @Override
            protected Object deepCopyNotNull(Object value) {
                return JacksonUtil.clone(value);
            }
        });
    }

    @Override
    public boolean areEqual(Object one, Object another) {
        if ( one == another ) {
            return true;
        }
        if ( one == null || another == null ) {
            return false;
        }
        return JacksonUtil.toJsonNode(JacksonUtil.toString(one)).equals(
                JacksonUtil.toJsonNode(JacksonUtil.toString(another)));
    }

    @Override
    public String toString(Object value) {
        return JacksonUtil.toString(value);
    }

    @Override
    public Object fromString(String string) {
        return JacksonUtil.fromString(string, jsonObjectClass);
    }

    @SuppressWarnings({ "unchecked" })
    @Override
    public <X> X unwrap(Object value, Class<X> type, WrapperOptions options) {
        if ( value == null ) {
            return null;
        }
        if ( String.class.isAssignableFrom( type ) ) {
            return (X) toString(value);
        }
        if ( Object.class.isAssignableFrom( type ) ) {
            return (X) JacksonUtil.toJsonNode(toString(value));
        }
        throw unknownUnwrap( type );
    }

    @Override
    public <X> Object wrap(X value, WrapperOptions options) {
        if ( value == null ) {
            return null;
        }
        return fromString(value.toString());
    }

}

Maintenant, vous devez déclarer le nouveau type au niveau de la classe ou dans un descripteur au niveau du package (=== ==) :

@TypeDef(name = "jsonb", typeClass = JsonBinaryType.class)

Et le mappage d'entité ressemblera à ceci:

@Type(type = "jsonb")
@Column(columnDefinition = "json")
private Location location;

Si vous utilisez Hibernate 5 ou une version ultérieure, le type JSON est enregistré automatiquement par Postgre92Dialect .

Sinon, vous devez l'enregistrer vous-même:

public class PostgreSQLDialect extends PostgreSQL91Dialect {

    public PostgreSQLDialect() {
        super();
        this.registerColumnType( Types.Java_OBJECT, "json" );
    }
}
5
Vlad Mihalcea

J'ai eu un problème similaire et je l'ai résolu en utilisant l'annotation @Externalizer et Jackson pour sérialiser/désérialiser les données (@Externalizer est une annotation spécifique à OpenJPA, vous devez donc vérifier auprès de votre implémentation JPA la possibilité similaire).

@Persistent
@Column(name = "params")
@Externalizer("toJSON")
private Params params;

Implémentation de la classe Params:

public class Params {
    private static final ObjectMapper mapper = new ObjectMapper();

    private Map<String, Object> map;

    public Params () {
        this.map = new HashMap<String, Object>();
    }

    public Params (Params another) {
        this.map = new HashMap<String, Object>();
        this.map.putAll(anotherHolder.map);
    }

    public Params(String string) {
        try {
            TypeReference<Map<String, Object>> typeRef = new TypeReference<Map<String, Object>>() {
            };
            if (string == null) {
                this.map = new HashMap<String, Object>();
            } else {
                this.map = mapper.readValue(string, typeRef);
            }
        } catch (IOException e) {
            throw new PersistenceException(e);
        }
    }

    public String toJSON() throws PersistenceException {
        try {
            return mapper.writeValueAsString(this.map);
        } catch (IOException e) {
            throw new PersistenceException(e);
        }
    }

    public boolean containsKey(String key) {
        return this.map.containsKey(key);
    }

    // Hash map methods
    public Object get(String key) {
        return this.map.get(key);
    }

    public Object put(String key, Object value) {
        return this.map.put(key, value);
    }

    public void remove(String key) {
        this.map.remove(key);
    }

    public Object size() {
        return map.size();
    }
}

HTH

0
Magic Wand