JPA Specification to filter entries based on a key in the jsonb map - spring-boot

I have an entity as follows:
#Entity
#Getter
#Setter
#NoArgsConstructor
#TypeDefs({
#TypeDef(name = "jsonb", typeClass = JsonbType.class)
})
public class Entity extends BasePersistableEntity<String> implements Serializable {
public static final String FIELD_NAME_SERVICES = "services";
public static final String FIELD_NAME_ORG_ID = "orgId";
#Column
public String orgId;
#Column
#Type(type = "jsonb")
public Map<String, String> services;
}
I want to write a query to fetch all the entities from a Postgres database that belong to an orgId and has a service in its services map.
For example something like,
select * from Entity where entity.ordId='abc' and 'xyz' in entity.services.keyset();
Currently, I have written the solution as follows:
#Override
public List<Entity> getEntitiesWithOrgIdAndService(String orgId, String service) {
return entityRepository.findEntityByOrgId(orgId).stream()
.filter(entity -> entity.services.containsKey(service)).collect(Collectors.toList());
}
Instead of this, I would like to filter it while getting it from the DB. How can I use JPA specification to do so?
Something like as follows:
#Override
public List<Proxy> getEntitiesWithOrgIdAndService(String orgId, String service) {
return proxyRepository.findAll(getSpecificationForEntity(orgId, service));
}
private Specification<Proxy> getSpecificationForProxyForEntity(String orgId, String service) {
return (Specification<Proxy>) (root, query, builder) -> {
List<Predicate> predicates = new ArrayList<>();
Predicate orgIdPredicate =
builder.equal(root.get(Entity.FIELD_NAME_ORG_ID), orgId);
// FIX NEEDED
/* Predicate servicePredicate =
builder.equal(builder.function("jsonb_extract_path_text",
String.class, root.<String>get(Entity.FIELD_NAME_SERVICES), service); */
Predicate orgIdServicePredicate = builder.and(orgIdPredicate, servicePredicate);
predicates.add(orgIdServicePredicate);
return builder.and(predicates.toArray(new Predicate[0]));
};
}

The below change solved the issue for me.
Predicate servicePredicate =
builder.equal(builder.function(Constants.FUNCTION_JSONB_EXTRACT_PATH_TEXT,
String.class, root.<String>get(Proxy.FIELD_NAME_SERVICES),
builder.literal(service)), String.valueOf(true));

Related

Limit search by id

I want to implement an endpoint which is used to search into table limited by class_id:
Table:
#Entity
#Table(name = "class_items")
public class ClassItems implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "id", unique = true, updatable = false, nullable = false)
private int id;
#Column(name = "class_id", length = 20)
private Integer classId;
#Column(name = "title", length = 75)
private String title;
}
#PostMapping("/{class_id}/find")
public Page<ClassCategoriesFullDTO> search(#PathVariable("class_id") Integer classId, #Valid ClassCategoriesSearchParams params, Pageable pageable) {
Page<ClassCategoriesFullDTO> list = classItemsRestService.findClassItemsByClassId(classId, params, pageable);
return list;
}
public Page<ClassCategoriesFullDTO> findClassItemsByClassId(Integer classId, ClassCategoriesSearchParams params, Pageable pageable) {
// Limit here queries by classId
Specification<Product> spec = (root, query, cb) -> {
List<Predicate> predicates = new ArrayList<>();
if (params.getTitle() != null) {
predicates.add(cb.equal(root.get("title"), params.getTitle()));
}
return cb.and(predicates.toArray(new Predicate[predicates.size()]));
};
return classItemsService.findAllByClassId(spec, pageable).map(classItemsMapper::toFullDTO);
}
#Service
#Transactional
public class ClassItemsServiceImpl implements ClassItemsService {
#PersistenceContext
private EntityManager entityManager;
private ClassItemsRepository dao;
#Autowired
public ClassItemsServiceImpl(ClassItemsRepository dao) {
this.dao = dao;
}
public Page<ClassItems> findAllByClassId(Specification spec, Pageable pageable) {
return this.dao.findAllByClassId(spec, pageable);
}
}
#Repository
public interface ClassItemsRepository extends JpaRepository<ClassItems, Long>, JpaSpecificationExecutor<ClassItems> {
Page<ClassItems> findAllByClassId(Specification spec, Pageable pageable);
}
I get error:
Parameter value [org.service.ClassItemsRestServiceImpl$$Lambda$1987/0x0000000801e21440#3c4de5a9] did not match expected type [java.lang.Integer (n/a)]; nested exception is java.lang.IllegalArgumentException: Parameter value [org.service.ClassItemsRestServiceImpl$$Lambda$1987/0x0000000801e21440#3c4de5a9] did not match expected type [java.lang.Integer (n/a)]",
Do you know how I can solve this issue?
You're getting this error because Spring Data is trying to generate a query based on the method name findAllByClassId, and so it expects an integer as the first parameter.
When working with specifications, you're supposed to use the methods provided by JpaSpecificationExecutor. Adding your own methods with specifications as parameters won't work. If you want to filter by classId, append the appropriate filter to the specification itself.
EDIT the solution is to add the extra condition when constructing the specification:
Specification<Product> spec = (root, query, cb) -> {
List<Predicate> predicates = new ArrayList<>();
if (params.getTitle() != null) {
predicates.add(cb.equal(root.get("title"), params.getTitle()));
}
predicates.add(cb.equal(root.get("classId"), classId));
return cb.and(predicates.toArray(new Predicate[predicates.size()]));
};
and then, in ClassItemsServiceImpl, call dao.findAll(spec)

Spring data jpa specification and pageable in #manytomany using join table repository

I have a use case to filter and paginate the record with #manytomany relation using a separate join table.
Below are the relation and entities
public class User {
private Long userId;
private String userName
#OneToMany(mappedBy = "user")
private List<UserRole> userRole;
}
public class Role {
private Long roleId;
private String roleName
#OneToMany(mappedBy = "role")
private List<UserRole> userRole;
}
public class UserRole{
private Long id;
private Integer active
#ManyToOne
#MapsId("userId")
private User user;
#ManyToOne
#MapsId("roleId")
private Role role;
}
#Repository
public interface UserRoleRepository extends
JpaRepository<UserRole, String>,
JpaSpecificationExecutor<UserRole> {
}
public class UserRoleSpecification implements Specification<UserRole>
{
private SearchCriteria criteria;
public RuleEntitySpecification(SearchCriteria criteria ) {
this.criteria = criteria;
}
#Override
public Predicate toPredicate(Root<UserRole> root,
CriteriaQuery<?> query,
CriteriaBuilder criteriaBuilder) {
if(criteria.getOperation().equalsIgnoreCase("eq")) {
if(root.get(criteria.getKey()).getJavaType() == String.class)
{
return criteriaBuilder.like(root.get(criteria.getKey()),
"%" + criteria.getValue() + "%");
} else {
return criteriaBuilder.equal(root.get(criteria.getKey()),
criteria.getValue());
}
}
return null;
}
}
public class SearchCriteria implements Serializable {
private String key;
private String operation;
private Object value;
}
UserRoleSpecificationBuilder specBuilder = new UserRoleSpecificationBuilder();
specBuilder.with("active", "eq" , 1); // giving us proper result
Specification<UserRole> spec = specBuilder.build();
Pageable paging = PageRequest.of(0, 5, Sort.by("user.userId"));
Page<UserRole> pagedResult = userRoleRepository.findAll(spec,paging);
But when we try to filter based on Rule/User table properties like userName/roleName specBuilder.with("user.userName", "eq" , "xyz");, I am getting following exception:
org.springframework.dao.InvalidDataAccessApiUsageException:
Unable to locate Attribute with the the given name
[user.userName] on this ManagedType
Kindly suggest if there is any way to achieve the filter using UserRole Join Table repository and specification
Pagination is also required hence using repository of Type UserRole JoinTable.
#Override
public Predicate toPredicate(Root<UserRole> root,
CriteriaQuery<?> query,
CriteriaBuilder criteriaBuilder) {
if (criteria.getOperation().equalsIgnoreCase("eq")) {
String key = criteria.getKey();
Path path;
if (key.contains(".")) {
String attributeName1 = key.split("\\.")[0];
String attributeName2 = key.split("\\.")[1];
path = root.get(attributeName1).get(attributeName2);
} else {
path = root.get(key);
}
if (path.getJavaType() == String.class) {
return criteriaBuilder.like(path, "%" + criteria.getValue() + "%");
} else {
return criteriaBuilder.equal(root.get(key), criteria.getValue());
}
}
return null;
}

Why the get request give empty response in Spring Boot?

I'm trying to make simple rest services which can save the data to h2 database using JPA and show the data in response, but when I try POST request, the data that saved is null even though when I check the h2 console, the ID is entered saved because it use #GeneratedValue, but other is null. also when I want try GET request, the response give me null json
#Entity
public class MS_Product {
#GeneratedValue
#Id
#Getter
private long productId;
#Getter #Setter
private String productName;
#Getter #Setter
private int productPrice;
#Getter #Setter
private int productStock;
#UpdateTimestamp
#Getter
private LocalDateTime updatedDate;
protected MS_Product() {
}
public MS_Product(long productId, String productName, int productPrice, int productStock, LocalDateTime updatedDate) {
super();
this.productId = productId;
this.productName = productName;
this.productPrice = productPrice;
this.productStock = productStock;
this.updatedDate = updatedDate;
}
}
public interface MS_ProductRepository extends JpaRepository<MS_Product, Long>{
}
#RestController
public class MS_ProductController {
#Autowired
MS_ProductRepository productRepository;
#GetMapping("/products")
public ResponseEntity<MS_Product> findAllProduct(){
try {
List<MS_Product> products = productRepository.findAll();
return new ResponseEntity(products, HttpStatus.OK);
}catch(Exception e){
return new ResponseEntity<>(null, HttpStatus.INTERNAL_SERVER_ERROR);
}
}
#PostMapping("/products")
public ResponseEntity<MS_Product> createProduct(#RequestBody MS_Product product){
try {
MS_Product savedProduct = productRepository.save(product);
return new ResponseEntity(product, HttpStatus.CREATED);
}catch(Exception e){
return new ResponseEntity(null, HttpStatus.EXPECTATION_FAILED);
}
}
}
Try
#Entity(name="your_table_name")
public class Student {
By design, the in-memory database is volatile and data will be lost when we restart the application.
We can change that behavior by using file-based storage. To do this we need to update the spring.datasource.url:
spring.datasource.url=jdbc:h2:file:/data/demo
Ref: https://www.baeldung.com/spring-boot-h2-database
You need
#Column(name = "productId")
on every your field which you need to map to table column

How to show object's update history with Auditing?

I've got a problem, I made a CRUD in springboot with MYSQL and now I want to create a method which will return update history of my object...
I have class like:
#Entity
#Table
#EntityListeners(AuditingEntityListener.class)
#JsonIgnoreProperties(value = {"createdAt", "updatedAt"}, allowGetters = true)
#Audited
public class Note implements Serializable
{
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Getter
#Setter
private Long id;
#NotBlank
#Getter
#Setter
private String title;
#Version
#Getter
#Setter
private long version;
#NotBlank
#Getter
#Setter
private String content;
#Column(nullable = false, updatable = false)
#Temporal(TemporalType.TIMESTAMP)
#CreatedDate
#Getter
#Setter
private Date createdAt;
#Column(nullable = false)
#Temporal(TemporalType.TIMESTAMP)
#LastModifiedDate
#Getter
#Setter
private Date updatedAt;
}
But I don't know how can I now create a HTTP call to show that history of updates by #Audited.
I found something like this: Find max revision of each entity less than or equal to given revision with envers
But I don't know how to implement it in my project...
#RestController
#RequestMapping("/api")
public class NoteController
{
#Autowired
NoteRevisionService noteRevisionService;
#Autowired
NoteRepository noteRepository;
// Get All Notes
#GetMapping("/notes")
public List<Note> getAllNotes() {
return noteRepository.findAll();
}
// Create a new Note
#PostMapping("/notes")
public Note createNote(#Valid #RequestBody Note note) {
return noteRepository.save(note);
}
// Get a Single Note
#GetMapping("/notes/{id}")
public Note getNoteById(#PathVariable(value = "id") Long noteId) {
return noteRepository.findById(noteId)
.orElseThrow(() -> new ResourceNotFoundException("Note", "id", noteId));
}
#GetMapping("/notes/{id}/version")
public List<?> getVersions(#PathVariable(value = "id") Long noteId)
{
return noteRevisionService.getNoteUpdates(noteId);
}
// Update a Note
#PutMapping("/notes/{id}")
public Note updateNote(#PathVariable(value = "id") Long noteId,
#Valid #RequestBody Note noteDetails) {
Note note = noteRepository.findById(noteId)
.orElseThrow(() -> new ResourceNotFoundException("Note", "id", noteId));
note.setTitle(noteDetails.getTitle());
note.setContent(noteDetails.getContent());
Note updatedNote = noteRepository.save(note);
return updatedNote;
}
// Delete a Note
#DeleteMapping("/notes/{id}")
public ResponseEntity<?> deleteNote(#PathVariable(value = "id") Long noteId) {
Note note = noteRepository.findById(noteId)
.orElseThrow(() -> new ResourceNotFoundException("Note", "id", noteId));
noteRepository.delete(note);
return ResponseEntity.ok().build();
}
}
getVersions its the call of function which Joe Doe sent me.
There: Repository
#Repository
public interface NoteRepository extends JpaRepository<Note, Long>
{
}
You can use AuditQuery for this. The getNoteUpdates method below returns a list of mappings. Each mapping contains an object state and the time of the update that led to that state.
#Service
#Transactional
public class NoteRevisionService {
private static final Logger logger = LoggerFactory.getLogger(NoteRevisionService.class);
#PersistenceContext
private EntityManager entityManager;
#SuppressWarnings("unchecked")
public List<Map.Entry<Note, Date>> getNoteUpdates(Long noteId) {
AuditReader auditReader = AuditReaderFactory.get(entityManager);
AuditQuery query = auditReader.createQuery()
.forRevisionsOfEntity(Note.class, false, false)
.add(AuditEntity.id().eq(noteId)) // if you remove this line, you'll get an update history of all Notes
.add(AuditEntity.revisionType().eq(RevisionType.MOD)); // we're only interested in MODifications
List<Object[]> revisions = (List<Object[]>) query.getResultList();
List<Map.Entry<Note, Date>> results = new ArrayList<>();
for (Object[] result : revisions) {
Note note = (Note) result[0];
DefaultRevisionEntity revisionEntity = (DefaultRevisionEntity) result[1];
logger.info("The content of the note updated at {} was {}", revisionEntity.getRevisionDate(), note.getContent());
results.add(new SimpleEntry<>(note, revisionEntity.getRevisionDate()));
}
return results;
}
}
Note that if you can restrict the query somehow (for example by filtering on a property), you should definitely do it, because otherwise performing the query can have a negative impact on the performance of your entire application (the size of the returned list might be huge if this object was often updated).
Since the class has been annotated with the #Service annotation, you can inject/autowire NoteRevisionService like any other regular Spring bean, particularly in a controller that handles a GET request and delegates to that service.
UPDATE
I didn't know that extra steps had to be taken to serialize a list of map entries. There may be a better solution but the following approach gets the job done and you can customize the format of the output revisionDate with a simple annotation.
You need to define another class, say NoteUpdatePair, like so:
public class NoteUpdatePair {
private Note note;
#JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss")
private Date revisionDate; // this field is of type java.util.Date (not java.sql.Date)
NoteUpdatePair() {}
public NoteUpdatePair(Note note, Date revisionDate) {
this.note = note;
this.revisionDate = revisionDate;
}
public Note getNote() {
return note;
}
public void setNote(Note note) {
this.note = note;
}
public Date getRevisionDate() {
return revisionDate;
}
public void setRevisionDate(Date revisionDate) {
this.revisionDate = revisionDate;
}
}
and now, instead of returning a list of map entries, you'll return a list of NodeUpdatePair objects:
#Service
#Transactional
public class NoteRevisionService {
private static final Logger logger = LoggerFactory.getLogger(NoteRevisionService.class);
#PersistenceContext
private EntityManager entityManager;
#SuppressWarnings("unchecked")
public List<NoteUpdatePair> getNoteUpdates(Long noteId) {
AuditReader auditReader = AuditReaderFactory.get(entityManager);
AuditQuery query = auditReader.createQuery()
.forRevisionsOfEntity(Note.class, false, false)
.add(AuditEntity.id().eq(noteId)) // if you remove this line, you'll get an update history of all Notes
.add(AuditEntity.revisionType().eq(RevisionType.MOD)); // we're only interested in MODifications
List<Object[]> revisions = (List<Object[]>) query.getResultList();
List<NoteUpdatePair> results = new ArrayList<>();
for (Object[] result : revisions) {
Note note = (Note) result[0];
DefaultRevisionEntity revisionEntity = (DefaultRevisionEntity) result[1];
logger.info("The content was {}, updated at {}", note.getContent(), revisionEntity.getRevisionDate());
results.add(new NoteUpdatePair(note, revisionEntity.getRevisionDate()));
}
return results;
}
}
Regarding your question about the service's usage, I can see that you've already autowired it into your controller, so all you need to do is expose an appropriate method in your NoteController:
#RestController
#RequestMapping("/api")
public class NoteController {
#Autowired
private NoteRevisionService revisionService;
/*
the rest of your code...
*/
#GetMapping("/notes/{noteId}/updates")
public List<NoteUpdatePair> getNoteUpdates(#PathVariable Long noteId) {
return revisionService.getNoteUpdates(noteId);
}
}
Now when you send a GET request to ~/api/notes/1/updates (assuming nodeId is valid), the output should be properly serialized.

NamedQuery and no entity mapping

I would like to achieve the following. I have a query and I would like to run it and return rows in a REST call.
I do not want to map the query to a physical table, how would I achieve this?
I use Spring Boot 1.5.2.
After some try and fixes, I got the following solution.
Create a POJO class, no #Entity annotation. You want to add packageScan instructions if it is not found.
public class ActivityReport1 {
#Column
private BigInteger id;
#Column
private String title;
//Only getters
public ActivityReport1(BigInteger id,
String title){
this.id = id;
this.title = title;
}
In a class which is annotated with #Entity create the resultset mapping
#SqlResultSetMappings({
#SqlResultSetMapping(name = "ActivityReport1Mapping",
classes = {
#ConstructorResult(targetClass = ActivityReport1.class, columns = {
#ColumnResult(name = "id"),
#ColumnResult(name = "title")
})
})
})
Add repository class
#Repository
#Transactional
public class IActivityReport1Repository {
#PersistenceContext
private EntityManager entityManager;
public List<ActivityReport1> getResults(String userLogin) {
Query query = entityManager.createNativeQuery(
"SELECT " +
"t.request_id as id, t.request_title as title " +
"FROM some_table t ", "ActivityReport1Mapping");
List<ActivityReport1> results = query.getResultList();
return results;
}
}
And finally, the service impl class.
#Service
#Transactional(propagation = Propagation.SUPPORTS, readOnly = true)
public class ActivityReport1ServiceImpl implements IActivityReport1Service {
private static final Logger _Logger = LoggerFactory.getLogger(ActivityReport1ServiceImpl.class);
#Autowired
private IActivityReport1Repository sessionFactory;
#Override
public List<ActivityReport1> runReport(String userLogin) {
List<ActivityReport1> reportRows = sessionFactory.getResults(userLogin);
return reportRows;
}
}
If you face with "Could not locate appropriate constructor", this means that on Java side it could not map db types to java types.
In my case I had to change id from Long to BigInteger and Timestamp to java.util.date.

Resources