2014-09-09 2 views
10

Abbiamo una tabella grande con un sacco di colonne. Dopo ci siamo trasferiti a MySQL Cluster, la tabella non può essere creato a causa di:JPA map JSON column to Java Object

ERROR 1118 (42000): dimensioni Row troppo grande. La dimensione massima della riga per il tipo di tabella utilizzato, senza contare i BLOB, è 14000. Ciò include l'overhead di archiviazione, controllare il manuale. È necessario modificare alcune colonne al testo o BLOB

Ad esempio:

@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; 
} 

È una tabella per memorizzare parametri di configurazione. Stavo pensando che possiamo combinare alcune colonne in una e archiviarla come oggetto JSON e convertirla in qualche oggetto Java.

Ad esempio:

@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; 
} 

Dove abbiamo definito:

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

Utilizzando questo possiamo combinare tutte le colonne in un unico e creare il nostro tavolo. Oppure possiamo dividere l'intera tabella in più tabelle. Personalmente preferisco la prima soluzione.

In ogni caso la mia domanda è come mappare la colonna Params che è testo e contiene una stringa JSON di un oggetto Java?

+0

Se si dispone di molti parametri di configurazione, basta usare semplice tavolo con 2 colonne: chiave e il valore e caricarlo per mappare. Se vuoi memorizzare parametri come JSON o XML, basta archiviarli/leggerli come testo e convertirli in seguito. – user1516873

+0

@Rad fa [this] (http://stackoverflow.com/questions/22637733/mysql-error-code-1118-row-size-too-large-8126-changing-some-columns-to-te) ti aiuta –

+0

@ user1516873 abbiamo considerato questa come la soluzione finale. Se non sbaglio, aumenta la complessità mentre si tenta di modificare i dati. Grazie comunque. – Rad

risposta

27

È possibile utilizzare un convertitore di APP a mappare l'Entità al database. Basta aggiungere un'annotazione simile a questo per il vostro campo params:

@Convert(converter = JpaConverterJson.class) 

e quindi creare la classe in un modo simile (questo converte un oggetto generico, si può decidere di specializzarsi esso):

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; 
    } 
    } 

} 

Ecco: è possibile utilizzare questa classe per serializzare qualsiasi oggetto su json nella tabella.

+0

Nota: questa soluzione non funziona se si utilizzano anche Hibernate Envers e una versione di Hibernate inferiore a 5 (che non è ancora disponibile al momento della scrittura). Vedi [HHH-9042] (https://hibernate.atlassian.net/browse/HHH-9042). – Flavin

+0

Se il json memorizzato nel DB è un array, che significa qualcosa come: [{...}, {...}, {...}], il convertitore genererà un'eccezione o il mappatore lo gestirà? –

+0

Eccellente ha funzionato con hibernate core/entitymanager di ibidazione della versione 4.3.6 stessa. –

0

Ho avuto un problema simile e l'ho risolto usando l'annotazione @Externalizer e Jackson per serializzare/deserializzare i dati (@Externalizer è un'annotazione specifica di OpenJPA, quindi è necessario verificare con l'implementazione JPA una simile possibilità).

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

Parametri implementazione della classe:

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

Grazie per la risposta. Utilizziamo "spring-data-jpa". Come dice Maven, dipende da org.eclipse.persistence e org.hibernate per JPA. E 'quello che hai notato? – Rad

+0

No, stiamo usando JPA basato su OpenJPA e questa annotazione è specifica per OpenJPA. Penso che Hibernate come provider JPA non fornisca tale funzionalità, solo se lo si utilizza come Hibernate (HQL) normale e non come JPA. E Spring Data JPA, come dice il nome, usa JPA ... –

1

Come ho spiegato nel this article, l'APP AttributeConverter è troppo limitata per mappare tipi di oggetti JSON, soprattutto se si desidera salvare loro come JSON binario.

Non è necessario creare tutti questi tipi manualmente, è possibile ottenere semplicemente loro via Maven centrale utilizzando la seguente dipendenza:

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

Per ulteriori informazioni, visitate il hibernate-types open-source project.

Ora, per spiegare come funziona.

Ho scritto an article su come mappare oggetti JSON sia su PostgreSQL che su MySQL.

per PostgreSQL, è necessario inviare l'oggetto JSON in forma binaria:

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); 
    } 

} 

Il JsonBinarySqlTypeDescriptor assomiglia a questo:

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() 
       ); 
      } 
     }; 
    } 
} 

e JsonTypeDescriptor in questo modo:

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()); 
    } 

} 

Ora, è necessario dichiarare il nuovo tipo su entrambe le classe vel o in un package-info.java pacchetto di livello descriptior:

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

E la mappatura entità sarà simile a questa:

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

Se stai usando Hibernate 5 o versione successiva, quindi il tipo JSON è registered automatically by Postgre92Dialect.

In caso contrario, è necessario registrarsi da soli:

public class PostgreSQLDialect extends PostgreSQL91Dialect { 

    public PostgreSQL92Dialect() { 
     super(); 
     this.registerColumnType(Types.JAVA_OBJECT, "json"); 
    } 
}