How to persist an entity as Json in MySql? - java

I'm using spring and hibernate to persist #Entity classes into a mysql database.
For one attribute, I want to persist the object as a String or Json field, rather than creating an additional table and using #OneToOne reference mappings or similar.
Take the following just as an example:
#Entity
public class Customer {
//#JsonObject
private List<Address> address;
}
public class Address {
private String street, number, city, zip, country;
}
Question: how could I tell hibernate to automatically save that address as string/json? And of course, when reading the string/json should be remapped into an Address object.

You can store jsons into mySQL tables:
CREATE TABLE `book` (
`id` mediumint(8) unsigned NOT NULL AUTO_INCREMENT,
`title` varchar(200) NOT NULL,
`tags` json DEFAULT NULL,
PRIMARY KEY (`id`)
)
Insert query:
INSERT INTO `book` (`title`, `tags`)
VALUES (
'ECMAScript 2015: A SitePoint Anthology',
'["JavaScript", "ES2015", "JSON"]'
);
In hibernate, you should create class (like "com.test.MyJsonType") that must implement org.hibernate.usertype. UserType interface where the nullSafeGet method should deserialize JSON to java object (using Jackson), the nullSafeSet serialize POJO to JSON and some other auxiliary methods.

For the moment I could solve it with AttributesConverter. Nice that Spring can automatically inject the jackson mapper into it:
#Entity
public class Customer {
#Convert(converter = AddressConverter.class)
#Lob
#Column(columnDefinition = "text")
private List<Address> address;
}
#Converter
public class AddressConverter implements AttributeConverter<List<Address>, String> {
private ObjectMapper objectMapper;
#Autowired //injected by spring
public AddressConverter(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
}
#Override
public String convertToDatabaseColumn(List<AddressConverter> list) {
try {
if (!CollectionUtils.isEmpty(list))
return objectMapper.writeValueAsString(list);
} catch (final JsonProcessingException e) {
LOGGER.error(e);
}
return null;
}
#Override
public List<Address> convertToEntityAttribute(String json) {
try {
if (StringUtils.isNotBlank(json))
return objectMapper.readValue(json, List.class);
} catch (final IOException e) {
LOGGER.error(e);
}
return null;
}
}

Related

How can I generalize JsonConverter for all classes?

I have an entity like below:
#Entity
#Table(name = "orders")
#Data
#Builder
#NoArgsConstructor
#AllArgsConstructor
public class Order {
#Convert(converter = UserInfoJsonConverter.class)
private UserInfo userInfo;
#Convert(converter = IndUserInfoJsonConverter.class)
private IndUserInfo indUserInfo;
#Convert(converter = DuplicateInfoJsonConverter.class)
private DuplicateInfo duplicateInfo;
#Convert(converter = PartnershipJsonConverter.class)
private PartnershipInfo partnerShipInfo;
#Convert(converter = SecretJsonConverter.class)
private SecretInfo secretInfo;
....
And as you see I have 5 fields as JSON string in DB. And also I have 5 JsonConverter implementations like below:
#Slf4j
#Converter(autoApply = true)
public class IndUserInfoJsonConverter implements AttributeConverter<IndUserInfo, String> {
private final ObjectMapper objectMapper = new ObjectMapper();
#Override
public String convertToDatabaseColumn(IndUserInfo attribute) {
try {
objectMapper.registerModule(new JavaTimeModule());
return objectMapper.writeValueAsString(attribute);
} catch (JsonProcessingException ex) {
log.error("Could not write view {} to json", attribute, ex);
throw new InternalServerErrorException("Could not write the object details into string", ex);
}
}
#Override
public IndUserInfo convertToEntityAttribute(String dbData) {
try {
objectMapper.registerModule(new JavaTimeModule());
return dbData == null ? null : objectMapper.readValue(dbData, IndUserInfo.class);
} catch (JsonProcessingException ex) {
log.error("Could not write view {} to json", dbData, ex);
throw new InternalServerErrorException("Could not convert input string into object", ex);
}
}
}
And my question is how can I generalize these implementations? I mean how can I get rid of code repeat? Because the only difference is IndUserInfo in here.
Since converters don't have access to the model type, you can't use this approach in a generic way. You could use a UserType which can inject the model type, but then you could also use the hibernate-types project for this purpose.
As of Hibernate 6, you don't need any of this, as it comes with JSON support out of the box. You just have to annotate the persistent attribute with #JdbcTypeCode(SqlTypes.JSON)

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

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.

Cassandra - unable to parse list of Tuple with Java driver

I am trying to access Tuple data structure I have stored in Cassandra with Mapper. But, I am unable to. I haven't found any example online.
This is the table and data I have created.
cqlsh:test> CREATE TABLE test.test_nested (id varchar PRIMARY KEY, address_mapping list<frozen<tuple<text,text>>>);
cqlsh:test> INSERT INTO test.test_nested (id, address_mapping) VALUES ('12345', [('Adress 1', 'pin1'), ('Adress 2', 'pin2')]);
cqlsh:test>
cqlsh:test> select * from test.test_nested;
id | address_mapping
-------+----------------------------------------------
12345 | [('Adress 1', 'pin1'), ('Adress 2', 'pin2')]
(1 rows)
My mapped class(using lombok for builder, getter, setter):
#Builder
#Table(keyspace = "test", name = "test_nested")
public class TestNested {
#PartitionKey
#Column(name = "id")
#Getter
#Setter
private String id;
#Column(name = "address_mapping")
#Frozen
#Getter
#Setter
private List<Object> address_mapping;
}
My Mapper class:
public class TestNestedStore {
private final Mapper<TestNested> mapper;
public TestNestedStore(Mapper<TestNested> mapper) {
this.mapper = mapper;
}
public void insert(TestNested userDropData) {
mapper.save(userDropData);
}
public void remove(String id) {
mapper.delete(id);
}
public TestNested findByUserId(String id) {
return mapper.get(id);
}
public ListenableFuture<TestNested> findByUserIdAsync(String id) {
return mapper.getAsync(id);
}
}
I am trying to access data in a test method as follows:
#Test
public void testConnection2(){
MappingManager manager = new MappingManager(scyllaDBConnector.getSession());
Mapper<TestNested> mapper = manager.mapper(TestNested.class);
TestNestedStore testNestedStore = new TestNestedStore(mapper);
ListenableFuture<TestNested> fut = testNestedStore.findByUserIdAsync("12345");
Futures.addCallback(fut, new FutureCallback<TestNested>() {
#Override
public void onSuccess(TestNested testNested) {
}
#Override
public void onFailure(Throwable throwable) {
System.out.println("Call failed");
}
});
}
Bit, I am unable to access the tuple. I get this error:
java.lang.IllegalArgumentException: Error while checking frozen types on field address_mapping of entity com.example.model.TestNested: expected List to be not frozen but was frozen
at com.datastax.driver.mapping.AnnotationChecks.validateAnnotations(AnnotationChecks.java:73)
at com.datastax.driver.mapping.AnnotationParser.parseEntity(AnnotationParser.java:81)
at com.datastax.driver.mapping.MappingManager.getMapper(MappingManager.java:148)
at com.datastax.driver.mapping.MappingManager.mapper(MappingManager.java:105)
I have also tried with private List<TupleValue> address_mapping;. But of no use!
How do I access Tuple values through object mapper of cassandra?
You define address_mapping as list<frozen<tuple<text,text>>>, that is, a list of frozen tuple values. To communicate this to the MappingManager, you can use the #FrozenValue attribute.
TestNested should look like:
#Builder
#Table(keyspace = "test", name = "test_nested")
public class TestNested {
...
#Column(name = "address_mapping")
#Frozen
#Getter
#Setter
#FrozenValue
private List<Object> address_mapping;
}
For defining the cassandra datatype of
map<text,frozen<tuple<text,text,int,text>>>
in java entity class mention the datatype as,
import com.datastax.driver.core.TupleValue;
#FrozenValue
private Map<String,TupleValue> event_driven;

Map JSON string column of a JPA entity to Java object automatically

I have a JPA entity object with following structure:
#Table(name="item_info")
class Item(){
#Id
#GeneratedValue(strategy=GenerationType.IDENTITY)
private Long id;
#Column(name="item_name")
private String itemName;
#Column(name="product_sku")
private String productSku;
#Column(name="item_json")
private String itemJsonString;
#Transient
private ItemJson itemJson;
//Getters and setters
}
The itemJsonString field contains a json string value such as '{"key1":"value1","key2":"value2"}'
And the itemJson field contains the corresponding object which maps to the json string.
I get this entity object from database as follows:
Item item = itemRepository.findOne(1L); // Returns item with id 1
Now, the itemJson field is null since it is a transient field. And I have to set it manually using Jackson's ObjectMapper as follows:
itemJson = objectMapper.readValue(item.getItemJsonString(), ItemJson.class);
How can I make it such that when I do itemRepository.findOne(), it returns an Item object with the itemJson field mapped to the json String automatically?
Your best bet would be to implement a javax.persistence.Converter. It would look something like:
import javax.persistence.AttributeConverter;
import javax.persistence.Converter;
#Converter(autoApply = true)
public class LocalDateAttributeConverter implements AttributeConverter<ItemJson, String> {
#Override
public String convertToDatabaseColumn(ItemJson entityValue) {
if( entityValue == null )
return null;
ObjectMapper mapper = new ObjectMapper();
return mapper.writeValueAsString(entityValue);
}
#Override
public ItemJson convertToEntityAttribute(String databaseValue) {
if( databaseValue == null )
return null;
ObjectMapper mapper = new ObjectMapper();
return mapper.readValue(databaseValue, ItemJson.class);
}
}
I've used this with WildFly and didn't have to do anything except have it be in the war file I was deploying.
Here is the full working version of AttributeConverter + JPA + Kotlin.
Entity Class
In my case, database was mysql (8.x), which supports JSON as the underlying data type for column definition, and we can apply a custom converter using #Convert annotation.
#Entity
data class Monitor (
#Id
val id: Long? = null,
#Column(columnDefinition = "JSON")
#Convert(converter = AlertConverter::class)
var alerts: List<Alert> = emptyList(),
var active: Boolean = false
)
Converter Definition
Attribute converter needs to specify the conversion mechanism from data to db and reverse. We are using Jackson to convert a java object into String format and vice versa.
#Converter(autoApply = true)
class AlertConverter : AttributeConverter<List<Alert>, String> {
private val objectMapper = ObjectMapper()
override fun convertToDatabaseColumn(data: List<Alert>?): String {
return if (data != null && !data.isEmpty())
objectMapper.writeValueAsString(data)
else ""
}
override fun convertToEntityAttribute(dbData: String?): List<Alert> {
if (StringUtils.isEmpty(dbData)) {
return emptyList()
}
return objectMapper.readValue(dbData, object : TypeReference<List<Alert>>() {})
}
}
You could postLoad callback for manipulating entity after it's loaded. So try something like this inside your entity class
#PostLoad
public void afterLoad() {
ObjectMapper mapper = new ObjectMapper();
itemJson = mapper.readValue(item.getItemJsonString(), ItemJson.class);
}

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;

Categories

Resources