how to store PostgreSQL jsonb using SpringBoot + JPA? - postgresql

I'm working on a migration software that will consume unknown data from REST services.
I already think about use MongoDB but I decide to not use it and use PostgreSQL.
After read this I'm trying to implement it in my SpringBoot app using Spring JPA but I don't know to map jsonb in my entity.
Tried this but understood nothing!
Here is where I am:
#Repository
#Transactional
public interface DnitRepository extends JpaRepository<Dnit, Long> {
#Query(value = "insert into dnit(id,data) VALUES (:id,:data)", nativeQuery = true)
void insertdata( #Param("id")Integer id,#Param("data") String data );
}
and ...
#RestController
public class TestController {
#Autowired
DnitRepository dnitRepository;
#RequestMapping(value = "/dnit", method = RequestMethod.GET)
public String testBig() {
dnitRepository.insertdata(2, someJsonDataAsString );
}
}
and the table:
CREATE TABLE public.dnit
(
id integer NOT NULL,
data jsonb,
CONSTRAINT dnit_pkey PRIMARY KEY (id)
)
How can I do this?
Note: I don't want/need an Entity to work on. My JSON will always be String but I need jsonb to query the DB

Tried this but understood nothing!
To fully work with jsonb in Spring Data JPA (Hibernate) project with Vlad Mihalcea's hibernate-types lib you should just do the following:
1) Add this lib to your project:
<dependency>
<groupId>com.vladmihalcea</groupId>
<artifactId>hibernate-types-52</artifactId>
<version>2.2.2</version>
</dependency>
2) Then use its types in your entities, for example:
#Data
#NoArgsConstructor
#Entity
#Table(name = "parents")
#TypeDef(name = "jsonb", typeClass = JsonBinaryType.class)
public class Parent implements Serializable {
#Id
#GeneratedValue(strategy = SEQUENCE)
private Integer id;
#Column(length = 32, nullable = false)
private String name;
#Type(type = "jsonb")
#Column(columnDefinition = "jsonb")
private List<Child> children;
#Type(type = "jsonb")
#Column(columnDefinition = "jsonb")
private Bio bio;
public Parent(String name, List children, Bio bio) {
this.name = name;
this.children = children;
this.bio = bio;
}
}
#Data
#NoArgsConstructor
#AllArgsConstructor
public class Child implements Serializable {
private String name;
}
#Data
#NoArgsConstructor
#AllArgsConstructor
public class Bio implements Serializable {
private String text;
}
Then you will be able to use, for example, a simple JpaRepository to work with your objects:
public interface ParentRepo extends JpaRepository<Parent, Integer> {
}
parentRepo.save(new Parent(
"parent1",
asList(new Child("child1"), new Child("child2")),
new Bio("bio1")
)
);
Parent result = parentRepo.findById(1);
List<Child> children = result.getChildren();
Bio bio = result.getBio();

You are making things overly complex by adding Spring Data JPA just to execute a simple insert statement. You aren't using any of the JPA features. Instead do the following
Replace spring-boot-starter-data-jpa with spring-boot-starter-jdbc
Remove your DnitRepository interface
Inject JdbcTemplate where you where injecting DnitRepository
Replace dnitRepository.insertdata(2, someJsonDataAsString ); with jdbcTemplate.executeUpdate("insert into dnit(id, data) VALUES (?,to_json(?))", id, data);
You were already using plain SQL (in a very convoluted way), if you need plain SQL (and don't have need for JPA) then just use SQL.
Ofcourse instead of directly injecting the JdbcTemplate into your controller you probably want to hide that logic/complexity in a repository or service.

There are already several answers and I am pretty sure they work for several cases. I don't wanted to use any more dependencies I don't know, so I look for another solution.
The important parts are the AttributeConverter it maps the jsonb from the db to your object and the other way around. So you have to annotate the property of the jsonb column in your entity with #Convert and link your AttributeConverter and add #Column(columnDefinition = "jsonb") as well, so JPA knows what type this is in the DB. This should already make it possible to start the spring boot application. But you will have issues, whenever you try to save() with the JpaRepository. I received the message:
PSQLException: ERROR: column "myColumn" is of type jsonb but
expression is of type character varying.
Hint: You will need to rewrite or cast the expression.
This happens because postgres takes the types a little to serious.
You can fix this by a change in your conifg:
datasource.hikari.data-source-properties: stringtype=unspecified
datasource.tomcat.connection-properties: stringtype=unspecified
Afterwards it worked for me like a charm, and here is a minimal example.
I use JpaRepositories:
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
#Repository
public interface MyEntityRepository extends JpaRepository<MyEntity, Integer> {
}
The Entity:
import javax.persistence.Column;
import javax.persistence.Convert;
public class MyEntity {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
protected Integer id;
#Convert(converter = MyConverter.class)
#Column(columnDefinition = "jsonb")
private MyJsonObject jsonContent;
}
The model for the json:
public class MyJsonObject {
protected String name;
protected int age;
}
The converter, I use Gson here, but you can map it however you like:
import javax.persistence.AttributeConverter;
import javax.persistence.Converter;
#Converter(autoApply = true)
public class MyConverter implements AttributeConverter<MyJsonObject, String> {
private final static Gson GSON = new Gson();
#Override
public String convertToDatabaseColumn(MyJsonObject mjo) {
return GSON.toJson(mjo);
}
#Override
public MyJsonObject convertToEntityAttribute(String dbData) {
return GSON.fromJson(dbData, MyJsonObject.class);
}
}
SQL:
create table my_entity
(
id serial primary key,
json_content jsonb
);
And my application.yml (application.properties)
datasource:
hikari:
data-source-properties: stringtype=unspecified
tomcat:
connection-properties: stringtype=unspecified

For this case, I use the above tailored converter class, you are free to add it in your library. It is working with the EclipseLink JPA Provider.
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.log4j.Logger;
import org.postgresql.util.PGobject;
import javax.persistence.AttributeConverter;
import javax.persistence.Converter;
import java.io.IOException;
import java.sql.SQLException;
import java.util.Map;
#Converter
public final class PgJsonbToMapConverter implements AttributeConverter<Map<String, ? extends Object>, PGobject> {
private static final Logger LOGGER = Logger.getLogger(PgJsonbToMapConverter.class);
private static final ObjectMapper MAPPER = new ObjectMapper();
#Override
public PGobject convertToDatabaseColumn(Map<String, ? extends Object> map) {
PGobject po = new PGobject();
po.setType("jsonb");
try {
po.setValue(map == null ? null : MAPPER.writeValueAsString(map));
} catch (SQLException | JsonProcessingException ex) {
LOGGER.error("Cannot convert JsonObject to PGobject.");
throw new IllegalStateException(ex);
}
return po;
}
#Override
public Map<String, ? extends Object> convertToEntityAttribute(PGobject dbData) {
if (dbData == null || dbData.getValue() == null) {
return null;
}
try {
return MAPPER.readValue(dbData.getValue(), new TypeReference<Map<String, Object>>() {
});
} catch (IOException ex) {
LOGGER.error("Cannot convert JsonObject to PGobject.");
return null;
}
}
}
Usage example, for an entity named Customer.
#Entity
#Table(schema = "web", name = "customer")
public class Customer implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
#Convert(converter = PgJsonbToMapConverter.class)
private Map<String, String> info;
public Customer() {
this.id = null;
this.info = null;
}
// Getters and setter omitted.

If you're using R2DBC you can use dependency io.r2dbc:r2dbc-postgresql, and use type io.r2dbc.postgresql.codec.Json in your member attributes of an entity class, e.g.:
public class Rule {
#Id
private String client_id;
private String username;
private String password;
private Json publish_acl;
private Json subscribe_acl;
}

Related

Build method in JPA with composite key

This is my entity with composite key:
#Entity
#Table(name = "course_ratings")
public class CourseRating {
#EmbeddedId
private CourseRatingKey id;
}
Where CourseRatingKey looks like this:
#Embeddable
public class CourseRatingKey implements Serializable {
#Column(name = "user_id")
private int userId;
#Column(name = "course_id")
private int courseId;
public CourseRatingKey() {
}
// getters, setters, equals(), hashCode()
}
And my JPA repository
#Repository
public interface CourseRatingRepository extends JpaRepository<CourseRating, CourseRatingKey> {
}
I am trying to build method that will return list of all CourseRating with given courseId property of CourseRatingKey. Below method doesn't work because JPA doesn't recognize it:
repository.findAllByIdCourseId(int id);
How can I build my method name to achieve my goal?
I have solved my problem by declaring this method in repository. I'm a bit confused as the other methods work without declaring.
#Repository
public interface CourseRatingRepository extends JpaRepository<CourseRating, CourseRatingKey> {
List<CourseRating> findAllByIdCourseId(Integer id);
}

When using discriminator value for single table strategy, the first inserted entity's discriminator value is null but the value is there in database

When using discriminator value for inheritance/single table strategy, the first inserted entity's discriminator value is null but the value is there in the database.
I have to restart the server so that the query result containes the discriminator value:
package entity;
import java.io.Serializable;
import javax.persistence.Column;
import javax.persistence.DiscriminatorColumn;
import javax.persistence.DiscriminatorType;
import javax.persistence.DiscriminatorValue;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Inheritance;
import javax.persistence.InheritanceType;
import javax.persistence.MappedSuperclass;
import javax.persistence.NamedQueries;
import javax.persistence.NamedQuery;
import javax.persistence.Table;
#Entity
#Inheritance(strategy = InheritanceType.SINGLE_TABLE)
#DiscriminatorColumn(name="user_type", discriminatorType = DiscriminatorType.STRING)
#DiscriminatorValue("Null")
#Table(name="ALLUSER")
#NamedQueries({
#NamedQuery(name = "User.findAll", query = "SELECT u FROM User u"),
#NamedQuery(name = "User.findByAccount", query = "SELECT u FROM User u WHERE u.account = :account")
})
public class User implements Serializable{
/**
*
*/
private static final long serialVersionUID = 1L;
private String account;
private String password;
private String userType;
public User() {
super();
}
public User(String account, String password) {
super();
this.account = account;
this.password = password;
}
#Id
#Column(name = "account")
public String getAccount() {
return account;
}
public void setAccount(String account) {
this.account = account;
}
#Column(name = "password")
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
#Column(name = "user_type", insertable = false, updatable = false, nullable = false)
public String getUserType() {
return userType;
}
public void setUserType(String userType) {
this.userType = userType;
}
#Override
public String toString() {
return account;
}
}
#Entity
#DiscriminatorValue("Normal")
#NamedQueries({
#NamedQuery(name = "NormalUser.findAll", query = "SELECT u FROM NormalUser u")
})
public class NormalUser extends User implements Serializable{
/**
*
*/
//private String account;
private static final long serialVersionUID = 1L;
private LinkedHashSet<Customer> customers;
public NormalUser() {
super();
}
#OneToMany(fetch=FetchType.EAGER, mappedBy="normalUser", cascade = {CascadeType.PERSIST, CascadeType.MERGE, CascadeType.REMOVE}) //eager can be optimized when deleting a normal user
public LinkedHashSet<Customer> getCustomers() {
return customers;
}
public void setCustomers(LinkedHashSet<Customer> customers) {
this.customers = customers;
}
// #Column(name = "account")
// //have to override in order to get account to use
// public String getAccount() {
// return account;
// }
//
// public void setAccount(String account) {
// this.account = account;
// }
}
If I just add a new normal user(child entity), then query this user whose user type will be null:
I use eclipse-link as the JPA implementation and Java EE three-tiered web architecture.
I know this definitely has something to do with the working of entity manager and persistence but I don't know the details. I also don't know how to resolve it. Any suggestion is welcome!
You are not setting the 'type' field within your entities, and JPA doesn't set it for you - not in the java object anyway. If it isn't set when you persist an entity, it will remain unset for as long as that entity is cached (locally or the shared EMF level cache). Restarting the app works because it clears the cache, forcing any fetches of existing entities to load from the database, where the type was set based on the discriminator column value.
You can set the type when creating the class, or force the data to be reloaded from the database by calling em.refresh on the instance. In this case though, it seems strange to even bother mapping the type column as a basic mapping - the getType method should just return the static discriminator value for the class, and you cannot change the type string anyway.

How to generate Postgres tables correctly from spring boot application?

i am developing a RESTFUL API using spring-boot for a future Angular Front-End. i am having this problem with creating my entities into postgres tables.
connectivity with the database was checked and all is fine. using mvn clean install & mvn spring-boot run commands generate normal tomcat deployment without any errors. however no tables are create
here's my code:
entity:
#Entity
#Table(name = "questions")
public class Question {
#Id
#GeneratedValue(generator = "question_generator")
#SequenceGenerator(
name = "question_generator",
sequenceName = "question_sequence",
initialValue = 1000
)
private Long id;
#NotBlank
#Size(min = 3, max = 100)
private String title;
#Column(columnDefinition = "text")
private String description;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public Question() {
}
}
application.propreties:
# ===============================
# DATABASE CONNECTION
# ===============================
spring.datasource.driver-class-name=org.postgresql.Driver
spring.datasource.url=jdbc:postgresql://localhost:5432/postgres
spring.datasource.username=postgres
spring.datasource.password=postgres
# ===============================
# JPA / HIBERNATE
# ===============================
spring.jpa.show-sql=true
spring.jpa.hibernate.ddl-auto=update
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect
# Fix Postgres JPA Error:
# Method org.postgresql.jdbc.PgConnection.createClob() is not yet implemented.
spring.jpa.properties.hibernate.temp.use_jdbc_metadata_defaults=false
and here's my repo:
import model.Question;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
#Repository
public interface QuestionRepository extends JpaRepository<Question, Long> {
}
i was able to resolve this issue by changing the entity package and made it visible to the app. now it works perfectly fine.
the main package name is: com.testAppBlaBla
entities's package should be com.testAppBlaBla.model
otherwise the entity won't be generated.
Try to change spring.jpa.hibernate.ddl-auto property to create value.

Picketlink with custom model and long Id

I have a existing Model and want to use it with Picketlink. But I am using Long as #Id field. But Picketlink expect this to be a String field. I have found some hints to use another entity which maps to the corresponding entity of my model. But actually I don't now how to do it.
I have a base class, which all entities derive from:
#MappedSuperclass
public abstract class AbstractEntity implements Serializable, Cloneable {
#Id
#Identifier
#Column(name = "SID")
private Long sid;
#Column(name = "INSERT_TIME")
private Date insertTime;
#Column(name = "UPDATE_TIME")
private Date updateTime;
// getters and setters
}
And a derived realm entity:
#Entity
#IdentityManaged(Realm.class)
public class RealmEntity extends AbstractEntity {
#AttributeValue
private String name;
#PartitionClass
private String typeName;
#ConfigurationName
private String configurationName;
#AttributeValue
private boolean enforceSSL;
#AttributeValue
private int numberFailedLoginAttempts;
// getters and setters
}
And the mapping class for Picketlink looks as follows:
#IdentityPartition(supportedTypes = {
Application.class,
User.class,
Role.class
})
public class Realm extends AbstractPartition {
#AttributeProperty
private boolean enforceSSL;
#AttributeProperty
private int numberFailedLoginAttempts;
private Realm() {
this(null);
}
public Realm(String name) {
super(name);
}
}
The PartitionManager is defined as follows:
builder
.named("default.config")
.stores()
.jpa()
.supportType(User.class, Role.class, Application.class, Realm.class)
.supportGlobalRelationship(Grant.class, ApplicationAccess.class)
.mappedEntity(App.class, AppUserRole.class, AppRole.class, AppUser.class, UserEntity.class, RelationshipIdentityTypeEntity.class, RealmEntity.class)
.addContextInitializer((context, store) -> {
if (store instanceof JPAIdentityStore) {
if (!context.isParameterSet(JPAIdentityStore.INVOCATION_CTX_ENTITY_MANAGER)) {
context.setParameter(JPAIdentityStore.INVOCATION_CTX_ENTITY_MANAGER, entityManager);
}
}
});
When I try to create a new Realm Hibernate throws an error while trying to load the Realm because the #Id is defined as Long but the #Identifier of the Picketlink model is a String.
this.shsRealm = new Realm(REALM_SHS_NAME);
this.shsRealm.setEnforceSSL(true);
this.shsRealm.setNumberFailedLoginAttempts(3);
this.partitionManager.add(this.shsRealm);
java.lang.IllegalArgumentException: Provided id of the wrong type for class de.logsolut.common.picketlink.model.RealmEntity. Expected: class java.lang.Long, got class java.lang.String
How can I map the JPA model correctly to Picketlink?

playframework JPA Error and DB design issue

I get the following error:
JPA error
A JPA error occurred (Unable to build EntityManagerFactory): #OneToOne or #ManyToOne on models.Issue.project references an unknown entity: models.Project
Here you can see my entities:
package models;
import java.util.*;
import javax.persistence.*;
import play.db.jpa.*;
import models.Issue;
import models.Component;
public class Project extends Model{
public String self;
#Id
public String key;
#OneToMany (mappedBy="Project", cascade=CascadeType.ALL)
public List<Component> components;
#OneToMany (mappedBy="Project", cascade=CascadeType.ALL)
public List<Issue> issues;
public Project(String self, String key) {
this.self = self;
this.key = key;
this.components = new ArrayList<Component>();
this.issues = new ArrayList<Issue>();
}
public Project addComponent(String self, int component_id, String name, int issuecount) {
Component newComponent = new Component(self, component_id, name, issuecount, this);
this.components.add(newComponent);
return this;
}
public Project addIssue(Date created, Date updated, String self, String key,
String type, Status status) {
Issue newIssue = new Issue(created, updated, self, key, type, status, this);
this.issues.add(newIssue);
return this;
}
}
and this is the other
package models;
import java.util.*;
import javax.persistence.*;
import play.db.jpa.*;
import models.Project;
import models.Status;
import models.Component;
#Entity
public class Issue extends Model {
#Id
public String key;
public Date created;
public Date updated;
public String self;
public String type;
#ManyToOne
public Status status;
#ManyToOne
public Project project;
#OneToMany
public List<Component> components;
public Issue(Date created, Date updated, String self, String key,
String type, Status status, Project project ) {
this.created = created;
this.updated = updated;
this.self = self;
this.key = key;
this.status = status;
this.type = type;
this.project=project;
this.components=new ArrayList<Component>();
}
public Issue addComponent(Component component) {
this.components.add(component);
return this;
}
}
I'm using Play 1.2.4 and Eclipse. Now my db is in mem.
I have also a second question. Ideally I need a db for each user and I want to delete the content of the tables every time the user logs in ( or logs out ) and populate the table again when the user logs in (this is because the information stored in my db must be in synch with service I'm connecting to ). How should I do?
I totally have no clue. Please help me.
public class Project extends Model
is missing the #Entity annotation
The "mappedBy" should reference the property in the other entity which is "project" and not "Project".