Spring Data Jpa One To One mapping with where clause - spring-boot

I have two tables and I need OneToOne mapping with where clause.
select * from person_details inner join address_details
on address_details.pid=person_details.pid AND person_details.exist_flag = 'Y' AND address_details.address_exist_flag = 'Y'
Table 1
public class PersonDetails {
#Id
private String pid;
#Column(name = "first_name")
private String firstName;
#Column(name = "last_name")
private String lastName;
#Column(name = "exist_flag")
private String existFlag;
#OneToOne(mappedBy = "personDetails", cascade = CascadeType.ALL)
#Where(clause = "addressExistFlag = 'Y'")
private AddressDetails addressDetails;
}
Table 2
#Data
#NoArgsConstructor
#Entity
#Table(name = "address_details")
public class AddressDetails {
#Id
private String pid;
private String street;
#Column(name = "address_exist_flag")
private String addressExistFlag;
#OneToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
#JoinColumn(name = "pid", insertable = false, updatable = false)
private PersonDetails personDetails;
}
I need data to be fetched if both addressExistFlag = 'Y' and existFlag = 'Y'.
With current scenario If I am trying to fetch data via spring batch read repository as below, only existFlag = 'Y' is considered. Is it because of incorrect mapping or the way I have used in spring batch
ReadRepository looks like below
public interface PersonDetailsRepository extends JpaRepository<PersonDetails, String> {
Page<PersonDetails> findByExistFlag(String existFlag, Pageable pageable);
}
Spring batch read repository looks like below
#Bean
RepositoryItemReader<PersonDetails> personDetailsItemReader() {
Map<String, Sort.Direction> sort = new HashMap<>();
sort.put("ExistFlag", Sort.Direction.ASC);
return new RepositoryItemReaderBuilder<PersonDetails>()
.repository(personDetailsRepository)
.methodName("findByExistFlag")
.arguments("Y")
.sorts(sort)
.name("personDetailsItemReader")
.build();
}

You are only querying for existsFlag.
You have to add the other Flag too:
public interface PersonDetailsRepository extends JpaRepository<PersonDetails, String> {
Page<PersonDetails> findByExistFlagAndAddressDetailsAddressExistFlag(
String existFlag, String addressExistFlag, Pageable pageable);
}
#Bean
RepositoryItemReader<PersonDetails> personDetailsItemReader() {
Map<String, Sort.Direction> sort = new HashMap<>();
sort.put("ExistFlag", Sort.Direction.ASC);
return new RepositoryItemReaderBuilder<PersonDetails>()
.repository(personDetailsRepository)
.methodName("findByExistFlagAndAddressDetailsAddressExistFlag")
.arguments("Y", "Y")
.sorts(sort)
.name("personDetailsItemReader")
.build();
}

Related

How to update JPA/Hibernate entities with Apache Camel

I have a spring boot project with apache camel (Using maven dependencies: camel-spring-boot-starter, camel-jpa-starter, camel-endpointdsl).
There are the following 3 entities:
#Entity
#Table(name = RawDataDelivery.TABLE_NAME)
#BatchSize(size = 10)
public class RawDataDelivery extends PersistentObjectWithCreationDate {
protected static final String TABLE_NAME = "raw_data_delivery";
private static final String COLUMN_CONFIGURATION_ID = "configuration_id";
private static final String COLUMN_SCOPED_CALCULATED = "scopes_calculated";
#Column(nullable = false, name = COLUMN_SCOPED_CALCULATED)
private boolean scopesCalculated;
#OneToMany(mappedBy = "rawDataDelivery", fetch = FetchType.LAZY)
private Set<RawDataFile> files = new HashSet<>();
#CollectionTable(name = "processed_scopes_per_delivery")
#ElementCollection(targetClass = String.class)
private Set<String> processedScopes = new HashSet<>();
// Getter/Setter
}
#Entity
#Table(name = RawDataFile.TABLE_NAME)
#BatchSize(size = 100)
public class RawDataFile extends PersistentObjectWithCreationDate {
protected static final String TABLE_NAME = "raw_data_files";
private static final String COLUMN_CONFIGURATION_ID = "configuration_id";
private static final String COLUMN_RAW_DATA_DELIVERY_ID = "raw_data_delivery_id";
private static final String COLUMN_PARENT_ID = "parent_file_id";
private static final String COLUMN_IDENTIFIER = "identifier";
private static final String COLUMN_CONTENT = "content";
private static final String COLUMN_FILE_SIZE_IN_BYTES = "file_size_in_bytes";
#ManyToOne(optional = true, fetch = FetchType.LAZY)
#JoinColumn(name = COLUMN_RAW_DATA_DELIVERY_ID)
private RawDataDelivery rawDataDelivery;
#Column(name = COLUMN_IDENTIFIER, nullable = false)
private String identifier;
#Lob
#Column(name = COLUMN_CONTENT, nullable = true)
private Blob content;
#Column(name = COLUMN_FILE_SIZE_IN_BYTES, nullable = false)
private long fileSizeInBytes;
// Getter/Setter
}
#Entity
#TypeDef(name = "jsonb", typeClass = JsonBinaryType.class)
#Table(name = RawDataRecord.TABLE_NAME, uniqueConstraints = ...)
public class RawDataRecord extends PersistentObjectWithCreationDate {
public static final String TABLE_NAME = "raw_data_records";
static final String COLUMN_RAW_DATA_FILE_ID = "raw_data_file_id";
static final String COLUMN_INDEX = "index";
static final String COLUMN_CONTENT = "content";
static final String COLUMN_HASHCODE = "hashcode";
static final String COLUMN_SCOPE = "scope";
#ManyToOne(optional = false)
#JoinColumn(name = COLUMN_RAW_DATA_FILE_ID)
private RawDataFile rawDataFile;
#Column(name = COLUMN_INDEX, nullable = false)
private long index;
#Lob
#Type(type = "jsonb")
#Column(name = COLUMN_CONTENT, nullable = false, columnDefinition = "jsonb")
private String content;
#Column(name = COLUMN_HASHCODE, nullable = false)
private String hashCode;
#Column(name = COLUMN_SCOPE, nullable = true)
private String scope;
}
What I try to do is to build a route with apache camel which selects all deliveries having the flag "scopesCalculated" == false and calculate/update the scope variable of all records attached to the files of this deliveries. This should happen in one database transaction. If all scopes are updated I want to set the scopesCalculated flag to true and commit the changes to the database (in my case postgresql).
What I have so far is this:
String r3RouteId = ...;
var dataSource3 = jpa(RawDataDelivery.class.getName())
.lockModeType(LockModeType.NONE)
.delay(60).timeUnit(TimeUnit.SECONDS)
.consumeDelete(false)
.query("select rdd from RawDataDelivery rdd where rdd.scopesCalculated is false and rdd.configuration.id = " + configuration.getId())
;
from(dataSource3)
.routeId(r3RouteId)
.routeDescription(configuration.getName())
.messageHistory()
.transacted()
.process(exchange -> {
RawDataDelivery rawDataDelivery = exchange.getIn().getBody(RawDataDelivery.class);
rawDataDelivery.setScopesCalculated(true);
})
.transform(new Expression() {
#Override
public <T> T evaluate(Exchange exchange, Class<T> type) {
RawDataDelivery rawDataDelivery = exchange.getIn().getBody(RawDataDelivery.class);
return (T)rawDataDelivery.getFiles();
}
})
.split(bodyAs(Iterator.class)).streaming()
.transform(new Expression() {
#Override
public <T> T evaluate(Exchange exchange, Class<T> type) {
RawDataFile rawDataFile = exchange.getIn().getBody(RawDataFile.class);
// rawDataRecordJpaRepository is an autowired interface by spring with the following method:
// #Lock(value = LockModeType.NONE)
// Stream<RawDataRecord> findByRawDataFile(RawDataFile rawDataFile);
// we may have many records per file (100k and more), so we don't want to keep them all in memory.
// instead we try to stream the resultset and aggregate them by 500 partitions for processing
return (T)rawDataRecordJpaRepository.findByRawDataFile(rawDataFile);
}
})
.split(bodyAs(Iterator.class)).streaming()
.aggregate(constant("all"), new GroupedBodyAggregationStrategy())
.completionSize(500)
.completionTimeout(TimeUnit.SECONDS.toMillis(5))
.process(exchange -> {
List<RawDataRecord> rawDataRecords = exchange.getIn().getBody(List.class);
for (RawDataRecord rawDataRecord : rawDataRecords) {
rawDataRecord.setScope("abc");
}
})
;
Basically this is working, but I have the problem that the records of the last partition will not be updated. In my example I have 43782 records but only 43500 are updated. 282 remain with scope == null.
I really don't understand the JPA transaction and session management of camel and I can't find some examples on how to update JPA/Hibernate entities with camel (without using SQL component).
I already tried some solutions but none of them are working. Most attempts end with "EntityManager/Session closed", "no transaction is in progress" or "Batch update failed. Expected result 1 but was 0", ...
I tried the following:
to set jpa(...).joinTransaction(false).advanced().sharedEntityManager(true)
use .enrich(jpa(RawDataRecord.class.getName()).query("select rec from RawDataRecord rec where rawDataFile = ${body}")) instead of .transform(...) with JPA repository for the records
using hibernate session from camel headers to update/save/flush entities: "Session session = exchange.getIn().getHeader(JpaConstants.ENTITY_MANAGER, Session.class);"
try to update over new jpa component at the end of the route:
.split(bodyAs(Iterator.class)).streaming()
.to(jpa(RawDataRecord.class.getName()).usePersist(false).flushOnSend(false))
Do you have any other ideas / recommendations?

How to search float fields as text in elastic using QueryBuilder

I ve document named plan that correspond plan entity
#Entity
#Table(name = "plan")
#Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
#org.springframework.data.elasticsearch.annotations.Document(indexName = "plan")
public class Plan extends AbstractAuditingEntity implements Serializable {
#Id
#GeneratedValue
#Column(name = "id")
#Field(type = FieldType.Text , fielddata = true)
private UUID id;
#NotNull
#Column(name = "name", nullable = false, unique = true)
private String name;
#Column(name = "description")
private String description;
#Column(name = "current_price")
#Field(type = FieldType.Text , fielddata = true )
private Float currentPrice;
}
Here my method search implementation
public Page<Plan> search(String query, Pageable pageable) {
NativeSearchQuery nativeSearchQuery = new NativeSearchQuery(queryStringQuery("*"+query+"*").defaultOperator(Operator.AND));
nativeSearchQuery.setPageable(pageable);
List<Plan> hits = elasticsearchTemplate
.search(nativeSearchQuery, Plan.class)
.map(SearchHit::getContent)
.stream()
.collect(Collectors.toList());
return new PageImpl<>(hits, pageable, hits.size());
}
Name and Description are searchable but float field isn't .
Marking as FieldType.Float doesn't give expected result .

I want to input boolean value in ChallengeDto

public class ChallengeDto {
private Long id;
private Category category;
private String title;
private String subTitle;
private boolean like;
private int totalScore;
private int requiredScore;
public ChallengeDto(Long id, Category category, String title, String subTitle, boolean like, int totalScore, int requiredScore) {
this.id = id;
this.category = category;
this.title = title;
this.subTitle = subTitle;
this.like = like;
this.totalScore = totalScore;
this.requiredScore = requiredScore;
}
}
I created challengeDto that include challenge's properties(id, category, title, subtitle, totalScore, requiredScore) and like property(can know that if i like challenge or not).
If I put like button, that information stored challengeLike table.
public class ChallengeLike {
#Id
#GeneratedValue
#Column(name = "challenge_like_id")
private Long id;
#ManyToOne(fetch = LAZY)
#JoinColumn(name = "user_id")
private User user;
#ManyToOne(fetch = LAZY)
#JoinColumn(name = "challenge_id")
private Challenge challenge;
private LocalDateTime createDate;
}
Now I'm trying to write a code to retrieve challengeDto that checks if I clicked like or not, but I'm having a problem... I can't think of what kind of code to make.
#Repository
#RequiredArgsConstructor
public class ChallengeDtoRepository {
private final EntityManager em;
#Transactional
public List<ChallengeDto> findChallenges(Long userId) {
return em.createQuery(
"select new " +
"com.example.candy.controller.challenge.ChallengeDto(c.id,c.category,c.title,c.subTitle,????,c.totalScore,c.requiredScore)" +
" from Challenge c" +
" left join ChallengeLike cl on c.id = cl.challenge.id" +
" and cl.user.id = : userId", ChallengeDto.class)
.setParameter("userId", userId)
.getResultList();
}
}
try to rename the field to likeDone or something different than like, it makes the code ambiguous.
However, just simply do:
cl.likeDone
which means:
return em.createQuery(
"select new " +
"com.example.random.demo.dto.ChallengeDto(c.id,c.category,c.title,c.subTitle,cl.likeDone,c.totalScore,c.requiredScore)" +
" from Challenge c" +
" left join ChallengeLike cl on c.id = cl.challenge.id" +
" where cl.user.id = : userId", ChallengeDto.class)
.setParameter("userId", userId)
.getResultList();
However, try to use JPA if you don't have any mandatory condition to use native query or jpql.
JPA implementation:
#Repository
public interface ChallengeLikeRepository extends JpaRepository<ChallengeLike, Long> {
List<ChallengeLike> findAllByUser_Id(long userId);
}
Just call the repository method from service layer and map to your required dto:
public List<ChallengeDto> findChallenges(Long userId) {
List<ChallengeLike> entities = this.repository.findAllByUser_Id(userId);
return entities.stream().map(this::mapToDto).collect(Collectors.toList());
}
The mapToDto() method converts the entity to corresponding ChallengeDto
private ChallengeDto mapToDto(ChallengeLike x) {
return ChallengeDto.builder()
.category(x.getChallenge().getCategory())
.id(x.getChallenge().getId())
.like(x.isLikeDone())
.requiredScore(x.getChallenge().getRequiredScore())
.subTitle(x.getChallenge().getSubTitle())
.title(x.getChallenge().getTitle())
.totalScore(x.getChallenge().getTotalScore())
.userId(x.getUser().getId())
.build();
}
For your convenience, some properties has been added or changed in some classes. The #Builder annotation has been added to the ChallengeDto class. The rest of the corresponding entity and other classes:
a) ChallengeLike.java
#Entity
#Data
public class ChallengeLike {
#Id
#GeneratedValue
#Column(name = "challenge_like_id")
private Long id;
#ManyToOne
#JoinColumn(name = "user_id")
#JsonIgnoreProperties("challengeLikes")
private User user;
#ManyToOne
#JoinColumn(name = "challenge_id")
#JsonIgnoreProperties("challengeLikes")
private Challenge challenge;
private boolean likeDone;
private LocalDateTime createDate;
}
b) Challenge.java
#Entity
#Data
public class Challenge {
#Id
private Long id;
private Category category;
private String title;
private String subTitle;
private int totalScore;
private int requiredScore;
#OneToMany(mappedBy = "challenge", cascade = CascadeType.ALL)
#JsonIgnoreProperties("challenge")
private List<ChallengeLike> challengeLikes = new ArrayList<>();
}
c) Category.java
public enum Category {
CAT_A,
CAT_B
}
Update
If you want to fetch Challenge entity instead of ChallengeLike and map that to ChallengeDto, first implement ChallangeRepository:
#Repository
public interface ChallengeRepository extends JpaRepository<Challenge, Long> {
}
Add the fetchType to EAGER in Challange Entity class:
#OneToMany(mappedBy = "challenge", cascade = CascadeType.ALL, fetch = FetchType.EAGER)
#JsonIgnoreProperties("challenge")
private List<ChallengeLike> challengeLikes = new ArrayList<>();
And to map the Challenge to ChallengeDto, you can add another mothod as follows:
private ChallengeDto mapToDto(Challenge x) {
return ChallengeDto.builder()
.category(x.getCategory())
.id(x.getId())
.like(!x.getChallengeLikes().isEmpty() && x.getChallengeLikes().get(0).isLikeDone())
.requiredScore(x.getRequiredScore())
.subTitle(x.getSubTitle())
.title(x.getTitle())
.totalScore(x.getTotalScore())
.userId(x.getUserId()) // if you have user reference in Challenge, remove this otherwise.
.build();
}
finally, to incorporate everything properly, change the caller:
public List<ChallengeDto> findChallenges(Long userId) {
List<Challenge> entities = this.repository.findAll();
List<ChallengeDto> entitiesWithoutChallengeLikes = entities.stream()
.filter(x -> x.getChallengeLikes() == null
|| x.getChallengeLikes().isEmpty())
.map(this::mapToDto).collect(Collectors.toList());
List<ChallengeDto> entitiesInferredFromChallengeLikes = entities.stream()
.filter(x -> x.getChallengeLikes() != null && !x.getChallengeLikes().isEmpty())
.flatMap(x -> x.getChallengeLikes().stream())
.map(this::mapToDto)
.collect(Collectors.toList());
entitiesInferredFromChallengeLikes.addAll(entitiesWithoutChallengeLikes);
return entitiesInferredFromChallengeLikes;
}
Final Update
Well, I finally understood properly what you expected. Adopt the following changes to the previous solution and you will get exactly what you want.
Change the 2 occurrence of the following in the findChallanges method:
.map(this::mapToDto)
To:
.map(x -> mapToDto(x, userId))
And the two mapToDto functions will be changed to follows:
private ChallengeDto mapToDto(ChallengeLike x, long userId) {
return ChallengeDto.builder()
.category(x.getChallenge().getCategory())
.id(x.getChallenge().getId())
.like(x.getUser().getId() == userId && x.isLikeDone())
.requiredScore(x.getChallenge().getRequiredScore())
.subTitle(x.getChallenge().getSubTitle())
.title(x.getChallenge().getTitle())
.totalScore(x.getChallenge().getTotalScore())
.userId(x.getUser().getId())
.build();
}
private ChallengeDto mapToDto(Challenge x, long userId) {
return ChallengeDto.builder()
.category(x.getCategory())
.id(x.getId())
.like(false)
.requiredScore(x.getRequiredScore())
.subTitle(x.getSubTitle())
.title(x.getTitle())
.totalScore(x.getTotalScore())
.userId(userId)
.build();
}

Hibernate #OneToMany relation cascade option not working

I design simple 1:N schema , Account(1):AccountProfileImage(N).
Below codes are entity codes.
// Account.java
#Entity
#Table(name = "account")
#Getter
#NoArgsConstructor(access = AccessLevel.PROTECTED)
#EntityListeners(AuditingEntityListener.class)
public class Account {
#GeneratedValue
#Id
#Column(name = "id")
private Long id;
#Column(name = "email", nullable = false)
private String email;
#Column(name = "password", nullable = false)
private String password;
#Column(name = "first_name", nullable = false)
private String firstName;
#Column(name = "last_name", nullable = false)
private String lastName;
#CreatedDate
#Column(name = "created_at")
private LocalDateTime createdAt;
#OneToMany(mappedBy ="account",cascade = CascadeType.ALL)
private final List<AccountProfileImage> profileImages= new ArrayList<>();
#Builder
public Account(String email,String firstName,String lastName,String password){
this.email=email;
this.firstName=firstName;
this.lastName=lastName;
this.password=password;
}
}
// AccountProfileImage.java
#Entity
#Table(name = "account_profile_image")
#NoArgsConstructor(access = AccessLevel.PROTECTED)
#Getter
#EntityListeners(AuditingEntityListener.class)
public class AccountProfileImage {
#Id
#GeneratedValue
private Long id;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "account_id")
private Account account;
#Column(name = "image_url")
private String imageURL;
#CreatedDate
#Column(name = "created_at")
private LocalDateTime createdAt;
#Builder
public AccountProfileImage (Account account,String imageURL){
this.account=account;
// this.account.addProfileImage(this);
this.imageURL=imageURL;
}
}
and this is test code for AccountProfileRepository code.
#Test
#Rollback(value = false)
public void saveAccountProfileImageTest() throws Exception {
// given
Account account = Account.builder()
.email("user#email.com")
.firstName("user")
.lastName("user")
.password("1234")
.build();
AccountProfileImage profileImage = AccountProfileImage.builder()
.account(account)
.imageURL("pathToURI")
.build();
AccountProfileImage profileImage2 = AccountProfileImage.builder()
.account(account)
.imageURL("pathToURI2")
.build();
accountRepository.save(account);
// when
List<AccountProfileImage> images = profileImageRepository.findAllByAccount_IdOrderByCreatedAtDesc(1L);
// then
// this assertion fail
assertThat(images.size()).isEqualTo(2);
}
What i want to expect find by List of images whose size is 2 because I add CscadeType.ALL in Account entity class and when creating AccountProfileImage object, I set account member variable in AccountProfileImage object.
this.account=account;
Did I something wrong?
I add below method in Account entity and 2 lines at test code, then it works fine. Do i have to do this everytime? Is there exist another better approach or best practice?
// Account Entity
public void addProfileImages(AccountProfileImage image){
this.profileImages.add(image);
}
// test code
account.addProfileImages(profileImage);
account.addProfileImages(profileImage2);
accountRepository.save(account);
// when
List<AccountProfileImage> images = profileImageRepository.findAllByAccount_IdOrderByCreatedAtDesc(1L);
// then
// this assertion pass
assertThat(images.size()).isEqualTo(2);
In Bi-directional relationships, you have to define the association on both ends of the relationship. To avoid any issues, you can update the helper addProfileImage(..) method to add the AccountProfileImage to the list and set account property of the image to the current account. This is the best practice as this way, the helper method will set up the association across both ends of the bi-directional relationship.
E.g.
public void addProfileImages(AccountProfileImage image){
this.profileImages.add(image); // Add image to profileImages
image.setAccount(this); // Set account property to the current account
}
Test:
#Test
#Rollback(value = false)
public void saveAccountProfileImageTest() throws Exception {
// given
Account account = Account.builder()
.email("user#email.com")
.firstName("user")
.lastName("user")
.password("1234")
.build();
AccountProfileImage profileImage = AccountProfileImage.builder()
.imageURL("pathToURI")
.build();
AccountProfileImage profileImage2 = AccountProfileImage.builder()
.imageURL("pathToURI2")
.build();
// Setup association
account.addProfileImage(profileImage);
account.addProfileImage(profileImage2);
accountRepository.save(account);
// when
List<AccountProfileImage> images = profileImageRepository.findAllByAccount_IdOrderByCreatedAtDesc(1L);
// then
// this assertion fail
assertThat(images.size()).isEqualTo(2);
}

Spring Boot ManyToMany jackson recursion

Person:
#Entity
#Table(name = "person")
public class Person{
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private int id;
#Column(name = "name", nullable = false, length = 30)
private String name;
#Column(name = "age", nullable = false, length = 2)
private int age;
#ManyToMany
private List<Person> friends;
}
PersonController:
#RestController
#RequestMapping("/person")
public class PersonController{
#Autowired
PersonService personService;
#RequestMapping(value = {"/findAll"}, method = RequestMethod.GET)
public Object findAll(){
List<Person> people = personService.findAll();
Map<String, Object> response = new HashMap<String, Object>();
response.put("msg", "王安生王person!");
response.put("people", people);
return response;
}
}
database:
table person
table person_friends
Why is the result of "/findAll" isinfinite loop?
This link Jackson – Bidirectional Relationships will help you to resovle your problem.
Sorry,I made a mistake.The database is not suitable and the business logic is incorrect.

Resources