SpringBoot: Persist nested JSON [using spring-boot-starter-data-rest + sql] - java

I have the following POST request:
{
"name": "Peter",
"lastName": "Smith",
"contact": {
"phone":"12345679",
"email": "peter#smith.com"
}
}
And I would like to store that in a SQL DB as follow:
| id (int) | name (varchar) | lastName (varchar) | contact (JSON) |
I'm using spring-boot-starter-data-rest so I only have the UserRepository and User Entity, which has an Embedded property contact
User.java
#Entity
#Table(name="user")
public class User {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#Column(name = "name")
private String title;
#Column(name = "lastName")
private String lastName;
#Embedded
#Column(name = "contact")
private Contact contact;
}
Contact.java
#Embeddable
public class Contact {
private String phone;
private String email;
}
UserRepository.java
public interface UserRepository extends JpaRepository<User, Integer> {
//
}
If I make a POST request I get an error, because (guess) I'm not converting Contact to JSON.
I've already tried adding a #Convert(converter = HashMapConverter.class) but I get an error.
HashMapConverter
public class HashMapConverter implements AttributeConverter<Object, String> {
private static final ObjectMapper om = new ObjectMapper();
#Override
public String convertToDatabaseColumn(Object attribute) {
try {
return om.writeValueAsString(attribute);
} catch (JsonProcessingException ex) {
//log.error("Error while transforming Object to a text datatable column as json string", ex);
return null;
}
}
#Override
public Object convertToEntityAttribute(String dbData) {
try {
return om.readValue(dbData, Object.class);
} catch (IOException ex) {
//log.error("IO exception while transforming json text column in Object property", ex);
return null;
}
}
}

i've got the same case for storing json field which is working perfectly. Please try :
Add dependency to pom.xml :
<dependency>
<groupId>com.vladmihalcea</groupId>
<artifactId>hibernate-types-52</artifactId>
<version>2.9.7</version>
</dependency>
Edit User class within :
#Entity
#TypeDef(name = "json", typeClass = JsonStringType.class)
public class User {
// other field here
#Type(type = "json")
#Column(columnDefinition = "json")
private Contact contact;
// getters, setters
}
Of course your database should support json type. For MariaDB for example you can refer to https://mariadb.com/kb/en/json-data-type/

You need to create an entity for Contact and then create a one to one relationship between the two. Check out this example.

Related

How to update MongoDB #DbRef embedded collection?

So, I am trying my hands-on MongoDB CRUD operations using spring-data-mongodb. Below are my model classes,
#Document(collection = "alumni_students")
public class AlumniStudent {
#Id
private String id;
private String firstName;
private String lastName;
private String email;
#DBRef
private AlumniDepartment alumniDepartment;
#DBRef
private List<AlumniSubject> alumniSubjects;
... getters/setters
#Document(collection = "alumni_department")
public class AlumniDepartment {
#Id
private String id;
private String departmentName;
private String location;
... getters/setters
#Document(collection = "alumni_subjects")
public class AlumniSubject {
#Id
private String id;
private String subjectName;
private int marks;
... getters/setters
I am using MongoRepository for individual collections for their operations like below,
#Repository
public interface AlumniStudentRepository extends MongoRepository<AlumniStudent, String> { }
#Repository
public interface AlumniDepartmentRepository extends MongoRepository<AlumniDepartment, String> {}
#Repository
public interface AlumniSubjectRepository extends MongoRepository<AlumniSubject, String> {}
I have so far done good while creation and getting the student details. The issue I am facing is while updating the student data. In that also specifically while updating the data, I am confused as hell.
Below is my update code from service layer,
#Autowired
AlumniStudentRepository alumniStudentRepo;
#Autowired
AlumniDepartmentRepository alumniDeptRepo;
#Autowired
AlumniSubjectRepository alumniSubjRepo;
public AlumniStudent updateStudent(AlumniStudent student, String id) {
Optional<AlumniStudent> fetchedStudent = alumniStudentRepo.findById(id);
**// UPDATE STUDENT DATA, WORKS FINE**
if (fetchedStudent.isPresent()) {
AlumniStudent studentFromDB = fetchedStudent.get();
studentFromDB.setFirstName(student.getFirstName());
studentFromDB.setLastName(student.getLastName());
studentFromDB.setEmail(student.getEmail());
**// UPDATE DEPARTMENT DATA, WORKS FINE**
if (student.getAlumniDepartment() != null) {
Optional<AlumniDepartment> deptData = alumniDeptRepo.findById(studentFromDB.getAlumniDepartment().getId());
if (deptData.isPresent()) {
AlumniDepartment alumniDepartment = deptData.get();
alumniDepartment.setDepartmentName(student.getAlumniDepartment().getDepartmentName());
alumniDepartment.setLocation(student.getAlumniDepartment().getLocation());
alumniDeptRepo.save(alumniDepartment);
studentFromDB.setAlumniDepartment(alumniDepartment);
}
}
**// UPDATE SUBJECTS ARRAY DATA.... HOW TO DO THIS?**
if (student.getAlumniSubjects() != null && !student.getAlumniSubjects().isEmpty()) {
// Problematic area. How to perform update of arraylist here?
}
return alumniStudentRepo.save(studentFromDB);
}
}
This is the URL to hit in postman :
localhost:8080/alumnistudents/60aa384ffbf1851f56c71bef
And this is the request body:
{
"firstName": "Babita",
"lastName": "Raman",
"email": "babita#gmail.com",
"alumniDepartment": {
"departmentName": "Android Developer",
"location": "Dubai"
},
"alumniSubjects": [
{
"subjectName": "Java",
"marks": 80
},
{
"subjectName": "Unit testing",
"marks": 60
},
{
"subjectName": "Docker",
"marks": 80
}
]
}
I tried some random code but ended up with
Cannot create a reference to an object with a NULL id.
Can someone help here with how to update the arrays data which is referenced as #DbRef ?
Thanks in advance everyone.

how to receive a json object with #RequestBody or #RequestParam

First of all I apologize, I am new using this technology and I really have many doubts.
I am trying to send a json object to my controller class, the problem is that with #RequestBody all the data arrives but the foreign keys arrive null. Example enter a new user with the id of a role that already exists in the BD, the user data arrives complete but the role ID arrives null
First I register the data of the role via POST and everything works perfectly with #RequestBody, but when I try to register a user with the role_id that is already saved in the database also using #RequestBody it is saved with the id_rol null
My user entity:
#Entity
public class Usuario implements Serializable{
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
private String correo;
private String pass;
private boolean activo;
#OneToOne(optional = false, cascade = CascadeType.ALL, fetch = FetchType.LAZY)
#JoinColumn(name="rol_id", nullable = false)
private Rol rol;
public Usuario(){}
public Usuario(String correo, String pass, boolean activo, Rol rol) {
this.correo = correo;
this.pass = pass;
this.activo = activo;
this.rol = rol;
}
/*getter y setter*/
My role entity:
#Entity
//#Inheritance(strategy = InheritanceType.SINGLE_TABLE)
public class Rol implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
private String nombre;
private boolean activo;
#OneToMany(mappedBy = "rol")
private List<Usuario> usuarios;
public Rol(){}
public Rol(String nombre, boolean activo, List<Usuario> usuarios) {
this.nombre = nombre;
this.activo = activo;
this.usuarios = usuarios;
}
/*getter y setter*/
User Repository:
#Repository
public interface UsuarioRepository extends JpaRepository<Usuario, Serializable> {
public abstract Usuario findByCorreo(String correo);
}
Role Repository:
public interface RolRepository extends JpaRepository<Rol, Serializable> {
}
User controller class:
#RestController
#RequestMapping("/v1")
public class UsuarioController {
#Autowired
UsuarioRepository usuarioRepository;
#PostMapping("/usuario/add")
public #ResponseBody ResponseEntity<String> crear(#Valid #RequestBody Usuario usuario) {
try{
usuarioRepository.save(usuario);
return new ResponseEntity<String>("Registro Exitoso..", HttpStatus.OK);
}catch (Exception e){
return new ResponseEntity<String>("Ha ocurrido un Error..", HttpStatus.BAD_REQUEST);
}
}
}
role controller class:
#RestController
#RequestMapping("/v1")
public class RolController {
#Autowired
RolRepository rolRepository;
#PostMapping("/rol/add")
public #ResponseBody ResponseEntity<String> crear(#Valid #RequestBody Rol rol) {
try{
rolRepository.save(rol);
return new ResponseEntity<String>("Registro Exitoso..", HttpStatus.OK);
}catch (Exception e){
return new ResponseEntity<String>("Ha ocurrido un Error..", HttpStatus.BAD_REQUEST);
}
}
}
This is what I send to my controller:
{
"correo": "pepito#gmail.com",
"pass": "1234",
"activo": "true",
"rol_id": "5"
}
And this is what I receive:
{
"correo": "pepito#gmail.com",
"pass": "1234",
"activo": true,
"rol_id": null
}
How should I receive this body with id_rol = 5?
I know that I am doing something wrong in #RequestBody, I appreciate any example you can provide as I have searched and I have not found that. Thank you..!
This is because you are sending a wrong JSON structure to your REST Controller.
In your Usuario class, the role_id is actually an object field with a name rol which represents Rol class.
So you need to pass Rol with id as an JSON object in your request:
{
"correo": "pepito#gmail.com",
"pass": "1234",
"activo": "true",
"rol": {
"id":"5"
}
}

JPA 2.1 Attribute Converter convert enum still insert int

i'm using spring data jpa with hibernate as provider.
i'm trying to persist my enums on varchar(enum.tostring) instead of (0,1,2)
my enum class:
public enum MagasinType {
PRINCIPAL {
#Override
public String toString() {
return "Principale".toUpperCase();
}
},
SECONDARY {
#Override
public String toString() {
return "Secondaire".toUpperCase();
}
},
MOBILE {
#Override
public String toString() {
return "Mobile".toUpperCase();
}
};
public abstract String toString();
}
my converter
#Converter(autoApply = true)
public class MagasinConverter implements attributeConverter <MagasinType,String>{
#Override
public String convertToDatabaseColumn(MagasinType magasinType) {
switch (magasinType){
case MOBILE:return "MOBILE";
case PRINCIPAL:return "PRINCIPAL";
case SECONDARY:return "SECONDARY";
default:throw new IllegalArgumentException("valeur incorrecte" + magasinType);
}
}
#Override
public MagasinType convertToEntityAttribute(String s) {
switch (s){
case "MOBILE": return MagasinType.MOBILE;
case "SECONDARY": return MagasinType.SECONDARY;
case "PRINCIPAL": return MagasinType.PRINCIPAL;
default:throw new IllegalArgumentException("valeur incorrecte" + s);
}}}
my entity
#Entity
#Table(name = "MAGASIN")
public class Magasin extends AbstractEntity {
#Column(name = "LIBELLE", nullable = false)
private String libelle;
#Column(name = "DESCR")
private String descr;
#Convert(converter = MagasinConverter.class)
private MagasinType type;
#Column(name = "LOCATION")
private String localisation;
#Version
private long version;
//getters setters omitted
}
my java config : https://gist.github.com/anonymous/480ef7a58cdcc50e7481
my app.properties : https://gist.github.com/anonymous/685eaca98fcba9c33872
and finally my test method : https://gist.github.com/anonymous/8bb60fee39a201558e19
please help me on it, i want to use #convert new jpa2.1 feature instead of
#enumerated
i tried to put the annotation on the getter and it works.
now i can call the #convert to convert enums to strings and visversa when pulling from database.
the same problem happened when i added #manytoOne on my class attribute, i got a weired problem, no column was added to the table entity.
but when i annotated the getter. every thing was ok.
please take a look at my github repo to further infos
https://github.com/zirconias/RFID_REWRITE

How to map a map JSON column to Java Object with JPA

We have a big table with a lot of columns. After we moved to MySQL Cluster, the table cannot be created because of:
ERROR 1118 (42000): Row size too large. The maximum row size for the used table type, not counting BLOBs, is 14000. This includes storage overhead, check the manual. You have to change some columns to TEXT or BLOBs
As an example:
#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;
}
It's a table for storing configuration parameters. I was thinking that we can combine some columns into one and store it as JSON object and convert it to some Java object.
For example:
#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;
}
Where we have defined:
public class Params implements Serializable
{
private ParamA parama;
private ParamB paramb;
}
By using this we can combine all columns into one and create our table. Or we can split the whole table into several tables. Personally I prefer the first solution.
Anyway my question is how to map the Params column which is text and contains JSON string of a Java object?
You can use a JPA converter to map your Entity to the database.
Just add an annotation similar to this one to your params field:
#Convert(converter = JpaConverterJson.class)
and then create the class in a similar way (this converts a generic Object, you may want to specialize it):
#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;
}
}
}
That's it: you can use this class to serialize any object to json in the table.
The JPA AttributeConverter is way too limited to map JSON object types, especially if you want to save them as JSON binary.
You don’t have to create a custom Hibernate Type to get JSON support, All you need to do is use the Hibernate Types OSS project.
For instance, if you're using Hibernate 5.2 or newer versions, then you need to add the following dependency in your Maven pom.xml configuration file:
<dependency>
<groupId>com.vladmihalcea</groupId>
<artifactId>hibernate-types-52</artifactId>
<version>${hibernate-types.version}</version>
</dependency>
Now, you need to declare the new type either at the entity attribute level or, even better, at the class level in a base class using #MappedSuperclass:
#TypeDef(name = "json", typeClass = JsonType.class)
And the entity mapping will look like this:
#Type(type = "json")
#Column(columnDefinition = "json")
private Location location;
If you're using Hibernate 5.2 or later, then the JSON type is registered automatically by MySQL57Dialect.
Otherwise, you need to register it yourself:
public class MySQLJsonDialect extends MySQL55Dialect {
public MySQLJsonDialect() {
super();
this.registerColumnType(Types.JAVA_OBJECT, "json");
}
}
And, set the hibernate.dialect Hibernate property to use the fully-qualified class name of the MySQLJsonDialect class you have just created.
If you need to map json type property to json format when responding to the client (e.g. rest API response), add #JsonRawValue as the following:
#Column(name = "params", columnDefinition = "json")
#JsonRawValue
private String params;
This might not do the DTO mapping for server-side use, but the client will get the property properly formatted as json.
It is simple
#Column(name = "json_input", columnDefinition = "json")
private String field;
and in mysql database your column 'json_input' json type
There is a workaround for those don't want write too much code.
Frontend -> Encode your JSON Object to string base64 in POST method, decode it to json in GET method
In POST Method
data.components = btoa(JSON.stringify(data.components));
In GET
data.components = JSON.parse(atob(data.components))
Backend -> In your JPA code, change the column to String or BLOB, no need Convert.
#Column(name = "components", columnDefinition = "json")
private String components;
In this newer version of spring boot and MySQL below code is enough
#Column( columnDefinition = "json" )
private String string;
I was facing quotes issue so I commented below line in my project
#spring.jpa.properties.hibernate.globally_quoted_identifiers=true
I had a similar problem, and solved it by using #Externalizer annotation and Jackson to serialize/deserialize data (#Externalizer is OpenJPA-specific annotation, so you have to check with your JPA implementation similar possibility).
#Persistent
#Column(name = "params")
#Externalizer("toJSON")
private Params params;
Params class implementation:
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
If you are using JPA version 2.1 or higher you can go with this case.
Link Persist Json Object
public class HashMapConverter implements AttributeConverter<Map<String, Object>, String> {
#Override
public String convertToDatabaseColumn(Map<String, Object> customerInfo) {
String customerInfoJson = null;
try {
customerInfoJson = objectMapper.writeValueAsString(customerInfo);
} catch (final JsonProcessingException e) {
logger.error("JSON writing error", e);
}
return customerInfoJson;
}
#Override
public Map<String, Object> convertToEntityAttribute(String customerInfoJSON) {
Map<String, Object> customerInfo = null;
try {
customerInfo = objectMapper.readValue(customerInfoJSON,
new TypeReference<HashMap<String, Object>>() {});
} catch (final IOException e) {
logger.error("JSON reading error", e);
}
return customerInfo;
}
}
A standard JSON object would represent those attributes as a HashMap:
#Convert(converter = HashMapConverter.class)
private Map<String, Object> entityAttributes;

How can I deserialize a JSON array of "name" "value" pairs to a Pojo using Jackson

I want to deserialize the following JSON object:
{
"id":"001",
"module_name":"Users",
"name_value_list":
{
"user_name": {"name":"user_name", "value":"admin"},
"full_name": {"name":"full_name", "value":"Lluís Pi"},
"city": {"name":"full_name", "value":"Barcelona"},
"postal_code": {"name":"postal_code", "value":"08017"},
...
}
}
into some Java object like this:
#JsonIgnoreProperties(ignoreUnknown = true)
#JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE)
public class UserEntry
{
private String id;
private String moduleName;
private Person nameValueList;
public String getId()
{
return id;
}
public String getModuleName()
{
return moduleName;
}
public Person getPerson()
{
return nameValueList;
}
}
where Person is the following class:
#JsonIgnoreProperties(ignoreUnknown = true)
#JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE)
class Person
{
private String userName;
private String fullName;
private String city;
private String postalCode;
}
using Jackson but I get a deserialization error.
If I change the type of field nameValueList to a Map all the deserialization process goes with no problem and I get a map where the key is the "name" value and the value is the "value" value.
So my question is: is there any simple, or no so simple, way to deserialize this kind of JSON object to a Java Pojo with properties prop_1, prop_2, prop_3and prop_4?
{
"name_value_list":
{
"prop_1": {"name":"prop_1", "value":"value_1"},
"prop_2": {"name":"prop_2", "value":"value_2"},
"prop_3": {"name":"prop_3", "value":"value_3"},
"prop_4": {"name":"prop_4", "value":"value_4"},
...
}
}
Not very simple and not very clean. However you can do it by implementing a any setter field for the JSON attributes in the Person class which don't match any attribute on your UserEntry POJO.
#JsonAnySetter
public void putUserField(String userKey, Map<String, String> userValue)
throws NoSuchFieldException {
String actualFieldName = getActualFieldName(userKey);
Field field = this.getClass().getDeclaredField(actualFieldName);
field.setAccessible(true);
ReflectionUtils.setField(field, this, userValue.get("value"));
}
private String getActualFieldName(String userKey) {
return CaseFormat.LOWER_UNDERSCORE.to(CaseFormat.LOWER_CAMEL, userKey);
}
In addition to that, I had to change the Jackson attributes for the Person class to
#JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.PUBLIC_ONLY,
getterVisibility = JsonAutoDetect.Visibility.NONE)
for it to work for attributes like "city" which don't need any name transformation because jackson tries to directly set the field which fails.

Categories

Resources