Spring-Data-JPA atomic insert with dependencies - spring

I want to insert Entities to a database from a scalable microservice. I tried #Lock(LockModeType.PESSIMISTIC_WRITE) to prevent from doubled entries. The problem is, I have dependencies on my Entities.
A basic example is:
TestEntity.java
public class TestEntity {
#GeneratedValue()
#Id
private Long id;
private String string;
#ManyToOne
private TestEntityParent testEntityParent;
}
TestEntityParent.java
public class TestEntityParent {
#GeneratedValue()
#Id
private Long id;
private String stringTwo;
#OneToMany(mappedBy = "testEntityParent")
private List<TestEntity> testEntities;
}
TestEnityRepository.java
public interface TestEnityRepository extends JpaRepository<TestEntity,Long> {
#Lock(LockModeType.PESSIMISTIC_WRITE)
TestEntity saveAndFlush(TestEntity testEntity);
Optional<TestEntity> findByStringAndTestEntityParentStringTwo(String string, String stringTwo);
}
TestEntityParentRepository.java
public interface TestEntityParentRepository extends JpaRepository<TestEntityParent, Long> {
#Lock(LockModeType.PESSIMISTIC_WRITE)
TestEntityParent save(TestEntityParent testEntityParent);
Optional<TestEntityParent> findByStringTwo(String stringTwo);
}
AtomicDbService.java
#Service
public class AtomicDbService {
#Autowired
TestEnityRepository testEnityRepository;
#Autowired
TestEntityParentRepository testEntityParentRepository;
#Transactional
public TestEntity atomicInsert(TestEntity testEntity) {
TestEntityParent testEntityParent = testEntityParentRepository.findByStringTwo(testEntity.getTestEntityParent().getStringTwo())
.orElse(testEntityParentRepository.save(testEntity.getTestEntityParent()));
return testEnityRepository.findByStringAndTestEntityParentStringTwo(
testEntity.getString(), testEntity.getTestEntityParent().getStringTwo()
).orElse(testEnityRepository
.save(
TestEntity.builder()
.string(testEntity.getString())
.testEntityParent(testEntityParent)
.build()
)
);
}
}
My test case:
#Test
#Transactional
public void testAtomicInsert(){
TestEntityParent testEntityParent = TestEntityParent.builder().stringTwo("testTwo").build();
TestEntity testEntity = TestEntity.builder().string("test").testEntityParent(testEntityParent).build();
atomicDbService.atomicInsert(testEntity);
System.out.println(testEnityRepository.findAll());
atomicDbService.atomicInsert(testEntity);
System.out.println(testEnityRepository.findAll());
atomicDbService.atomicInsert(testEntity);
System.out.println(testEnityRepository.findAll());
System.out.println(testEnityRepository.findAll());
}
I get the following answer:
[TestEntity(id=2, string=test, testEntityParent=TestEntityParent(id=1, stringTwo=testTwo, testEntities=null))]
[TestEntity(id=2, string=test, testEntityParent=TestEntityParent(id=1, stringTwo=testTwo, testEntities=null)), TestEntity(id=3, string=test, testEntityParent=TestEntityParent(id=1, stringTwo=testTwo, testEntities=null))]
and an error:
query did not return a unique result: 2;
Without dependencies everything works fine.
UPDATE:
Adding #Lock(LockModeType.PESSIMISTIC_WRITE) to the find method leads to
Feature not supported: "MVCC=TRUE && FOR UPDATE && JOIN"; SQL statement:
... same applies to
#Lock(LockModeType.PESSIMISTIC_WRITE)
#Query("SELECT e from TestEntity e join e.testEntityParent p where e.string = :string and p.stringTwo = :stringTwo ")
Optional<TestEntity> findWhatever(#Param("string") String string, #Param("stringTwo") String stringTwo);
... since for update is always generated.

Apparently, it was a stupid mistake I needed to replace orElse with orElseGet and a lambda and everything worked, even without all those #Lock, etc - tricks.
Still I don't understand what exactly went wrong with the transactions and why.

Related

QuerySyntaxException with enum

I have a UserAssignmentRole class like this :
#Data
#Entity
public class UserAssignmentRole {
...
#Enumerated(EnumType.STRING)
public Role role;
}
And the Role is enum, it looks like this:
public enum Role{
admin,
member,
pending
}
Now when in my repository I try to query to select all with role of admin, it gives me error:
#Query("select uar from UserAssignmentRole uar where uar.role=Role.admin")
public List<UserAssignmentRole> listAdmin(Long userID, Long assignmentID);
How this can be solved?
Error : org.hibernate.hql.internal.ast.QuerySyntaxException: Invalid path: 'Role.admin'
Full error : https://pastebin.com/tk9r3wDg
It is a strange but intended behaviour of Hibernate since 5.2.x
An enum value is a constant and you're using a non-conventional naming (lowercase)
Take a look at this issue and Vlad Mihalcea's long explanation of the performance penalty.
If you’re using non-conventional Java constants, then you’ll have to set the hibernate.query.conventional_java_constants configuration property to false. This way, Hibernate will fall back to the previous behavior, treating any expression as a possible candidate for a Java constant.
You can try not to write this sql by yourself but with repository create code like this:
#Repository
public interface UserAssignmentRolelRepository extends JpaRepository<UserModel, Long>{
public List<UserAssignmentRole> findByRole(Role role);
}
And then:
#Autowired
UserAssignmentRolelRepository repository ;
public void someMethod(){
List<UserAssignmentRole> userAssignmentRoles = repository.findByRole(Role.admin);
}
UPDATE 1
As it was point out in this answer: non-conventional naming. You can change labels in your enum to uppercase.
public enum Role{
Admin,
Member,
Pending
}
and then:
#Query("select uar from UserAssignmentRole uar where uar.role=com.example.package.Role.Admin")
public List<UserAssignmentRole> listAdmin(Long userID, Long assignmentID);
UPDATE 2
But if you really want to have lowercase in DB.
It requires more code to change. Enum change to:
public enum Role{
Admin("admin"),
Member("member"),
Pending("pending");
private String name;
Role(String name) {
this.name = name;
}
public String getName() { return name; }
public static Role parse(String id) {
Role role = null; // Default
for (Role item : Role.values()) {
if (item.name.equals(id)) {
role = item;
break;
}
}
return role;
}
}
In UserAssignmentRole
// #Enumerated(EnumType.STRING)
#Convert(converter = RoleConverter.class)
private Role role;
And additional class:
import javax.persistence.AttributeConverter;
import javax.persistence.Converter;
#Converter(autoApply = true)
public class RoleConverter implements AttributeConverter<Role, String> {
#Override
public String convertToDatabaseColumn(Role role) {
return role.getName();
}
#Override
public Role convertToEntityAttribute(String dbData) {
return Role.parse(dbData);
}
}

RestController: returning Resource<> makes FetchType.LAZY to behave as FetchType.EAGER

I am having below mappings as Company having 1:N relation with CompanyFunds
#Entity
public class Company{
#Id
private Integer companyId;
private String name;
#OneToMany(mappedBy = "company")
private List<CompanyFund> companyFunds;
}
#Entity
public class CompanyFunds{
#Id
private Integer fundId;
private String name;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "company_id")
private Company company;
}
I am using Spring-data-Jpa for my persistence layer and below are the controller and service methods:
//controller
#GetMapping(value = "/{companyId}")
public Resource<Company> find(#PathVariable Integer companyId) {
Resource<Company> companyResource = companyService.find(companyId);
return companyResource;
}
//service
public Resource<CompanyTypeOther> find(Integer companyId) {
Company company = companyRepository.findById(companyId);
return restResourceAssembler.toResource(company);
}
#Component
public class RestResourceAssembler implements ResourceAssembler<T, Resource<T>> {
private EntityLinks entityLinks;
public RestResourceAssembler(EntityLinks entityLinks) {
this.entityLinks = entityLinks;
}
#Override
public Resource<T> toResource(T entity) {
Resource<T> resource = new Resource<>(entity);
resource.add(entityLinks.linkToSingleResource(entity.getClass(), entity.getId()).withSelfRel());
return resource;
}
}
Now the weird thing is, until the return companyResource;(in controller) doesn't get executed, the companyResource contains null for companyFunds i.e the LAZY loading is working fine till that point. But the moment the return companyResource; executes, something goes inside the Spring and the Select statement for CompanyFund gets fired. I debugged the steps and below is the code(try block) responsible for this:
public class ServletInvocableHandlerMethod extends InvocableHandlerMethod {
......
public void invokeAndHandle(ServletWebRequest webRequest, ModelAndViewContainer mavContainer,
........other code
try {
this.returnValueHandlers.handleReturnValue(
returnValue, getReturnValueType(returnValue), mavContainer, webRequest);
}
catch (Exception ex) {
if (logger.isTraceEnabled()) {
logger.trace(formatErrorForReturnValue(returnValue), ex);
}
throw ex;
}
}
.....
}
There are no toString() declared in entities, also no getCompanyFund() called. Can't understand what Spring is doing with returnValue above, so that some getters(or something) are getting called.
One more thing I noticed is, this problem only occurs when the Resource<Company> is returned. If I return Company from the controller, nothing unexpected happens. Lazy loading works fine.
Since I want to lazy load the entity, a little fix/hack solved the problem for me(as of now).
#JsonIgnore
#OneToMany(mappedBy = "company")
private List<CompanyFund> companyFunds;
#JsonIgnore prevents LAZY loaded entity from being serialized. So I guess the jackson is the culprit here.
This is not a fix is my view but just a hack to do the thing. Still waiting someone from Spring team to reply.

How does Spring's JPARepository and #Transactional behave together?

I have two methods (in a Spring boot application) that handle an entity. The entity has two fields, both boolean isDefault and isPdfGenerated. The first method (which is called from a controller) changes the isDefault flag when a new entity is created while the second one (called from a #Scheduled annotated method) changes the isPdfGenrated after it generates a pdf file for that entity.
My problem is that sometimes the second method finds entities with the isPdfGenerated flag set to false even though the file has been generated and saved in the database.
Both the methods have the #Transactional annotation and the repository interface for the entity extends JpARepository.
My guess is that the first method loads the entity from the database before the second method does but saves the entity after the second method does its job, thus overriding the isPdfGenerated flag.
Is this possible ? If the answer is yes, how should one handle such cases ? Shouldn't JPARepository handle the case when an entity gets updated from an external source ?
Bellow is some code to better illustrate the situation.
MyController:
#Controller
#RequestMapping("/customers")
public class MyController {
#Autowired
private EntityService entityService;
#RequestMapping(value = "/{id}/changeDefault", method = RequestMethod.POST)
public String changeDefault(#PathVariable("id") Long customerId, #ModelAttribute EntityForm entityForm, Model model) {
Entity newDefaultEntity = entityService.updateDefaultEntity(customerId, entityForm);
if (newDefaultEntity == null)
return "redirect:/customers/" + customerId;
return "redirect:/customers/" + customerId + "/entity/default;
}
}
EntityService:
import org.springframework.transaction.annotation.Transactional;
#Service
public class EntityService {
#Autowired
private EntityRepository entityRepository;
#Autowired
private CustomerRepository customerRepository;
#Transactional
public Entity updateDefaultEntity(Long customerId, submittedData) {
Customer customer = customerRepository.findById(customerId);
if(customer == null)
return customer; // I know there are better ways to do this
Entity currentDefaultEntity = entityRepository.findUniqueByCustomerAndDefaultFlag(customer, true);
if(currentDefaultEntity == null)
return null; // I know there are better ways to do this also
Entity newDefaultEntity = new Entity();
newDefaultEntity.setField1(submittedData.getField1());
newDefaultEntity.setField2(submittedData.getField2());
newDefaultEntity.setCustomer(customer);
oldDefaultEntity.setDefaultFlag(false);
newDefaultEntity.setDefaultFlag(true);
entityRepository.save(newDefaultEntity);
}
#Transactional
public void generatePdfDocument(Entity entity) {
Document pdfDocument = generateDocument(entity);
if(pdfDocument == null)
return;
documentRepository.save(pdfDocument);
entity.setPdfGeneratedFlag(true);
entityRepository.save(entity);
}
}
ScheduledTasks:
#Component
public class ScheduledTasks {
private static final int SECOND_IN_MILLISECONDS = 1000;
private static final int MINUTE_IN_SECONDS = 60;
#Autowired
private EntityRepository entityRepository;
#Autowired
private DocumentService documentService;
#Scheduled(fixedDelay = 20 * SECOND_IN_MILLISECONDS)
#Transactional
public void generateDocuments() {
List<Quotation> quotationList = entityRepository.findByPdfGeneratedFlag(false);
for(Entity entity : entitiesList) {
documentService.generatePdfDocument(entity);
}
}
}
DocumentService:
#Service
public class DocumentService {
#Autowired
private EntityRepository entityRepository;
#Autowired
private DocumentRepository documentRepository;
#Transactional
public void generatePdfDocument(Entity entity) {
Document pdfDocument = generateDocument(entity);
if(pdfDocument == null)
return;
documentRepository.save(pdfDocument);
entity.setPdfGeneratedFlag(true);
entityRepository.save(entity);
}
}
EntityRepository:
#Repository
public interface EntityRepository extends JpaRepository<Entity, Long> {
Entity findById(#Param("id") Long id);
List<Entity> findByPdfGeneratedFlag(#Param("is_pdf_generated") Boolean pdfGeneratedFlag);
Entity findUniqueByCustomerAndDefaultFlag(
#Param("customer") Customer customer,
#Param("defaultFlag") Boolean defaultFlag
);
}
DocumentRepository:
#Repository
public interface DocumentRepository extends JpaRepository<Document, Long> {
Document findById(#Param("id") Long id);
}
Entity:
#Entity
#Table(name = "entities")
#JsonIdentityInfo(generator = ObjectIdGenerators.IntSequenceGenerator.class, property = "id")
public class Entity {
private Long id;
private boolean defaultFlag;
private boolean pdfGeneratedFlag;
private String field1;
private String field2;
private Customer customer;
public Entity() { }
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "id")
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
#Column(name = "is_default")
public boolean isDefaultFlag() {
return defaultFlag;
}
public void setDefaultFlag(boolean defaultFlag) {
this.defaultFlag = defaultFlag;
}
#Column(name = "is_pdf_generated")
public boolean isPdfGeneratedFlag() {
return pdfGeneratedFlag;
}
public void setPdfGeneratedFlag(boolean pdfGeneratedFlag) {
this.pdfGeneratedFlag = pdfGeneratedFlag;
}
#Column(name = "field_1")
public String getField1() {
return field1;
}
public void setField1(String field1) {
this.field1 = field1;
}
#Column(name = "field_2")
public String getField2() {
return field2;
}
public void setField2(String field2) {
this.field2 = field2;
}
#ManyToOne
#JoinColumn(name = "customer_id", referencedColumnName = "id", nullable = false)
public Customer getCustomer() {
return customer;
}
public void setCustomer(Customer customer) {
this.customer = customer;
}
#Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Entity quotation = (Entity) o;
return id != null ? id.equals(entity.id) : entity.id == null;
}
#Override
public int hashCode() {
return id != null ? id.hashCode() : 0;
}
#Override
public String toString() {
return "Entity{" +
"id=" + id +
", pdfGeneratedFlag=" + pdfGeneratedFlag +
", defaultFlag=" + defaultFlag +
", field1=" + field1 +
", field2=" + field2 +
", customer=" + (customer == null ? null : customer.getId()) +
"}";
}
}
I have omitted the other classes because they are either POJOs ( EntityForm ) or the same as other domain model classes ( Document ).
If you're talking about a row on the database that is getting updated by another process after the first process has read it but before it has been updated, then you need to put in some sort of optimistic locking strategy.
This will be handled by the underlying ORM api (e.g. Hibernate or Eclipselink) rather than Spring Data (which will just handle an optimistic locking errors thrown by the ORM).
Have a look at this article. Bear in mind that if you want optimistic locking you need some way of determining a row's version. In JPA this is normally done using a column annotated with the #Version tag.
https://vladmihalcea.com/hibernate-locking-patterns-how-does-optimistic-lock-mode-work/

No composite key property found for type error in Spring JPA2

I have an error in spring JPA
org.springframework.data.mapping.PropertyReferenceException: No property CompanyId found for type CompanyUserDetail!
#Embeddable
public class CompanyUserKey implements Serializable {
public CompanyUserKey() {
}
#Column(name = "company_id")
private UUID companyId;
#Column(name = "user_name")
private String userName;
public UUID getCompanyId() {
return companyId;
}
public void setCompanyId(UUID companyId) {
this.companyId = companyId;
}
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
}
#Entity
#Table(name = "company_user_detail")
public class CompanyUserDetail {
#EmbeddedId
CompanyUserKey companyUserkey;
public CompanyUserKey getCompanyUserkey() {
return companyUserkey;
}
public void setCompanyUserkey(CompanyUserKey companyUserkey) {
this.companyUserkey = companyUserkey;
}
}
I am trying to access below method Service layer
#Component
public interface CompanyUserRepository extends JpaRepository<CompanyUserDetail, CompanyUserKey> {
public List<CompanyUserDetail> findByCompanyId(UUID companyId);
}
How can I achieve this ?
Thanks
Since in java model your CompanyUserKey is a property in the CompanyUserDetail class, I believe you should use full path (companyUserkey.companyId) to reach companyId:
public List<CompanyUserDetail> findByCompanyUserkeyCompanyId(UUID companyId);
Also note that you have a naming inconsistency: field in CompanyUserDetail is named companyUserkey instead of companyUserKey.
Assuming you are not using spring-data-jpa's auto generated implementations, your method contents might look something like the following:
FROM CompanyUserDetail c WHERE c.companyUserKey.companyId = :companyId
Now simply provide that query to the EntityManager
entityManager.createQuery( queryString, CompanyUserDetail.class )
.setParameter( "companyId", companyId )
.getResultList();
The key points are:
Query uses a named bind parameter called :companyId (not the leading :).
Parameter values are bound in a secondary step using setParameter method variants.
createQuery uses a second argument to influence type safety so that the return value from getResultList is a List<CompanyUserDetail> just like you requested.
Looking at spring-data-jpa's implementation however, I suspect it could look like this:
public interface CustomerUserRepository
extends JpaRepository<CompanyUserDetail, CompanyUserKey> {
#Query("select c FROM CompanyUserDetail c WHERE c.companyUserKey.companyId = :companyId")
List<CompanyUserDetail> findByCompanyId(#Param("companyId") UUID companyId);
}

Is it possible to avoid typecast on custom #Query?

Imagine that we have an entity:
#Entity
public class Person implements Serializable {
#Id
private String name;
private Long age;
private Boolean isMad;
...
}
And a repository with a trivial (and unnecessary) example for a custom query:
#Repository
public interface PersonRepository extends PagingAndSortingRepository<Info, String> {
#Query("select p.isMad, count(*) from Person p group by p.isMad")
List<Object> aggregateByMadness();
}
Now to parse this List we need to do something like this:
for (Object element : list) {
Object[] result = (Object[]) element;
Boolean isMad = (Boolean) result[0];
Long count = (Long) result[1];
}
which is a pain, can we cast the result of the query directly to List of a POJO?
Yes, you could use the JPQL construction expression:
package com.foo;
public class Madness {
public Madness(boolean isMad, Number count) { /* ...*/ }
}
And in your repository:
#Query("select new com.foo.Madness(p.isMad, count(*)) from Person p group by p.isMad")
List<Madness> aggregateByMadness();

Resources