web-dev-qa-db-fra.com

Persistance d'un objet JSON avec Hibernate et JPA

J'essaie de stocker un objet JSON dans la base de données MySQL au démarrage de printemps. Je sais que je fais quelque chose de mal, mais je ne peux pas comprendre ce que c'est parce que je suis assez nouveau au printemps. 

J'ai un noeud final où j'obtiens l'objet JSON suivant (via HTTP PUT) et je dois le stocker dans une base de données pour que l'utilisateur puisse le récupérer plus tard (via HTTP GET). 

{
  "A": {
    "Name": "Cat",
    "Age": "1"
  },
  "B": {
    "Name": "Dog",
    "Age": "2"
  },
  "C": {
    "Name": "Horse",
    "Age": "1"
  }
}

Notez que dans le cas ci-dessus, le nombre _ {declés de l'objet peut varier. En raison de cette exigence, j'utilise une HashMap pour intercepter l'objet dans le contrôleur. . 

@RequestMapping(method = RequestMethod.POST)
    public String addPostCollection(@RequestBody HashMap<String, Animal> hp) {

        hp.forEach((x, y) -> {
            postRepository.save(hp.get(x));
        });

        return "OK";

    }

Comme vous pouvez le constater dans la méthode, je peux effectuer une itération sur HashMap et conserver chaque objet Animal dans la base de données. Mais je cherche un moyen de conserver toute la HashMap dans un seul enregistrement. J'ai lu un peu et ils me suggèrent d'utiliser un mappage @ManyToMany

Quelqu'un peut-il m'indiquer dans une direction de persister la HashMap d'une manière différente? (ou utilisez-vous le @ManyToMany la seule et bonne façon de faire cela?) 

5
Fawzan

Comme je l'ai expliqué dans cet article , il est très facile de conserver un objet JSON à l'aide de Hibernate.

Vous n’avez pas à créer tous ces types manuellement, vous pouvez simplement obtenir les 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.

En supposant que vous ayez l'entité suivante:

@Entity(name = "Book")
@Table(name = "book")
@TypeDef(
    name = "jsonb-node", 
    typeClass = JsonNodeBinaryType.class
)
public class Book {

    @Id
    @GeneratedValue
    private Long id;

    @NaturalId
    private String isbn;

    @Type( type = "jsonb-node" )
    @Column(columnDefinition = "jsonb")
    private JsonNode properties;

    //Getters and setters omitted for brevity
}

Remarquez deux choses dans l'extrait de code ci-dessus:

  • le @TypeDef est utilisé pour définir un nouveau type personnalisé Hibernate, jsonb-node, qui est géré par la JsonNodeBinaryType
  • l'attribut properties a un type de colonne jsonb et il est mappé en tant que Jackson JsonNode

La JsonNodeBinaryType est implémentée comme ceci:

public class JsonNodeBinaryType
    extends AbstractSingleColumnStandardBasicType<JsonNode> {

    public JsonNodeBinaryType() {
        super( 
            JsonBinarySqlTypeDescriptor.INSTANCE, 
            JsonNodeTypeDescriptor.INSTANCE 
        );
    }

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

La JsonBinarySqlTypeDescriptor se présente comme suit:

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

Le code source AbstractJsonSqlTypeDescriptor peut être visualisé dans cet article .

Désormais, la JsonNodeTypeDescriptor est responsable de la transformation de la JsonNode en diverses représentations pouvant être utilisées par le pilote JDBC sous-jacent lors de la liaison de paramètres ou lors de l'extraction de l'objet JSON à partir de la ResultSet sous-jacente.

public class JsonNodeTypeDescriptor
        extends AbstractTypeDescriptor<JsonNode> {

    public static final JsonNodeTypeDescriptor INSTANCE = 
        new JsonNodeTypeDescriptor();

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

    @Override
    public boolean areEqual(JsonNode one, JsonNode 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(JsonNode value) {
        return JacksonUtil.toString(value);
    }

    @Override
    public JsonNode fromString(String string) {
        return JacksonUtil.toJsonNode(string);
    }

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

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

}

C'est tout!

Maintenant, si vous enregistrez une entité:

Book book = new Book();
book.setIsbn( "978-9730228236" );
book.setProperties(
    JacksonUtil.toJsonNode(
        "{" +
        "   \"title\": \"High-Performance Java Persistence\"," +
        "   \"author\": \"Vlad Mihalcea\"," +
        "   \"publisher\": \"Amazon\"," +
        "   \"price\": 44.99" +
        "}"
    )
);

entityManager.persist( book );

Hibernate va générer l'instruction SQL suivante:

INSERT INTO
    book 
(
    isbn, 
    properties, 
    id
) 
VALUES
(
    '978-9730228236', 
    '{"title":"High-Performance Java Persistence","author":"Vlad Mihalcea","publisher":"Amazon","price":44.99}',  
    1
)

Et vous pouvez aussi le charger et le modifier:

Session session = entityManager.unwrap( Session.class );

Book book = session
    .bySimpleNaturalId( Book.class )
    .load( "978-9730228236" );

LOGGER.info( "Book details: {}", book.getProperties() );

book.setProperties(
    JacksonUtil.toJsonNode(
        "{" +
        "   \"title\": \"High-Performance Java Persistence\"," +
        "   \"author\": \"Vlad Mihalcea\"," +
        "   \"publisher\": \"Amazon\"," +
        "   \"price\": 44.99," +
        "   \"url\": \"https://www.Amazon.com/High-Performance-Java-Persistence-Vlad-Mihalcea/dp/973022823X/\"" +
        "}"
    )
);

Hibernate prenant caare de la déclaration UPDATE pour vous:

SELECT  b.id AS id1_0_
FROM    book b
WHERE   b.isbn = '978-9730228236'

SELECT  b.id AS id1_0_0_ ,
        b.isbn AS isbn2_0_0_ ,
        b.properties AS properti3_0_0_
FROM    book b
WHERE   b.id = 1

-- Book details: {"price":44.99,"title":"High-Performance Java Persistence","author":"Vlad Mihalcea","publisher":"Amazon"}

UPDATE
    book 
SET
    properties = '{"title":"High-Performance Java Persistence","author":"Vlad Mihalcea","publisher":"Amazon","price":44.99,"url":"https://www.Amazon.com/High-Performance-Java-Persistence-Vlad-Mihalcea/dp/973022823X/"}'
WHERE
    id = 1

Tout le code est disponible sur GitHub .

16
Vlad Mihalcea

Votre JSON est bien structuré, il n'est donc généralement pas nécessaire de conserver toute la carte dans un seul enregistrement. Vous ne pourrez pas utiliser les fonctions de requête Hibernate/JPA et plus encore.

Si vous voulez vraiment conserver la carte entière dans un seul enregistrement, vous pouvez conserver la carte dans sa représentation sous forme de chaîne et, comme déjà proposé, utiliser un analyseur JSON comme Jackson pour reconstruire votre carte Hash

@Entity
public class Animals {

  private String animalsString;

  public void setAnimalsString(String val) {
    this.animalsString = val;
  }

  public String getAnimalsString() {
    return this.animalsMap;
  }

  public HashMap<String, Animal> getAnimalsMap() {
    ObjectMapper mapper = new ObjectMapper();
    TypeReference<HashMap<String,Animal>> typeRef = new TypeReference<HashMap<String,Animal>>() {};
    return mapper.readValue(animalsString, typeRef); 
  }

}

Votre classe d'animaux:

public class Animal {

  private String name;
  private int age;

  /* getter and setter */
  /* ... */
}

Et vous pouvez changer votre méthode de contrôleur pour

@RequestMapping(method = RequestMethod.POST)
public String addPostCollection(@RequestBody String hp) {
  Animals animals = new Animals();
  animals.setAnimalsString(hp);
  animalsRepository.save(hp);
  return "OK";
}
2
kaaas

Vous pouvez utiliser FasterXML (ou similaire) pour analyser le Json dans un objet réel (vous devez définir la classe) et utiliser Json.toJson(yourObj).toString() pour récupérer la chaîne Json. Cela simplifie également le travail avec les objets, car votre classe de données peut aussi avoir des fonctionnalités.

1
Petre Popescu

Un animal est un enregistrement. Vous enregistrez plusieurs enregistrements, pas un seul enregistrement. Vous pouvez valider plusieurs enregistrements en une seule transaction. Voir: Comment conserver beaucoup d’entités (JPA)

0
Peter Šály