I had a method in a ProjectRepository that looked something like this:
findByUserID(int userID);
This would list all projects that reference a certain user. Because of other requirements, I now need to reference the User from the Project, instead of just the userID, like so:
#ManyToOne
#JoinColumn(name = "userID", referencedColumnName = "userID")
private User user;
Which used to just be:
int userID;
So I replaced the method with:
findByUser(User user);
But this requires an extra call to the database to get the User object for a given userID. Is there a way to use the first method with my new class? If I just keep the first method, I get a
org.springframework.data.mapping.PropertyReferenceException: No property ID found for type User! Traversed path: Project.user.
Is this possible without writing a custom query?
Assuming the User class contains is defined as:
public class User {
private int userId;
public User(int userId) {
this.userId = userId;
}
//getters setters ommitted
}
and the Project class has a User object in it like so:
public class Project {
private User user;
public Project() {
}
//rest ommitted
}
It should according to the documentation be able to write a custom query in the repository that access the nested properties like project.user.userId by writing
findByUserUserId(int userId);
Or at least that's what I understood from reading the docs here
Try using the #PrimaryKeyJoinColumn annotation.
Related
I've a spring boot application which uses Hibernate as an ORM and DGS framework as the graphql engine. I've been struggling with finding ways to initialize a lazy loaded collection, the proper way. I've the following scenario:
application.properties
# The below has been set to false to get rid of the anti-pattern stuff it introduces
spring.jpa.open-in-view=false
...
#Entity
public class User {
#Id
#GeneratedValue
private UUID id;
#OneToMany(mappedBy = "user", fetch = FetchType.LAZY)
private List<Article> articles;
...
}
#Entity
public class Article {
#Id
#GeneratedValue
private UUID id;
#ManyToOne(optional = false, fetch = FetchType.LAZY)
private User user;
...
}
My User data fetcher looks something like this:
#DgsComponent
public class UserDataFetcher {
#Autowired
private UserService userService;
#DgsQuery
public User getUserById(#InputArgument UUID id) {
return userService.findById(id);
}
...
}
My UserService looks something like this:
#Service
public class UserServiceImpl implements UserService {
#Autowired
private UserRepository userRepository;
#Override
public User findById(UUID id) {
return userRepository.findById(id).orElseThrow(DgsEntityNotFoundException::new);
}
...
}
Now, I only want to initialize/load my articles collections from the DB when the user asks for it in the graphql query. For that purpose I created a child resolver for my articles which only executes when a user asks for the article in the query. My UserDataFetcher started looking like this:
#DgsComponent
public class UserDataFetcher {
#Autowired
private UserService userService;
#DgsQuery
public User getUserById(#InputArgument UUID id) {
return userService.findById(id);
}
#DgsData(parentType = "User", field = "articles")
public List<Article> getArticle(DgsDataFetchingEnvironment dfe) {
User user = dfe.getSource();
Hibernate.initialize(user.getArticles());
return user.getArticles();
}
...
}
But, the above started throwing exceptions telling me that Hibernate couldn't find an open session for the above request. Which made sense because there wasn't any so I put a #Transactional on top of my child resolver and it started looking like this:
#DgsComponent
public class UserDataFetcher {
#Autowired
private UserService userService;
#DgsQuery
public User getUserById(#InputArgument UUID id) {
return userService.findById(id);
}
#DgsData(parentType = "User", field = "articles")
#Transactional
public List<Article> getArticle(DgsDataFetchingEnvironment dfe) {
User user = dfe.getSource();
Hibernate.initialize(user.getArticles());
return user.getArticles();
}
...
}
However, the above didn't work either. I tried moving this #Transactional into my service layer as well but even then it didn't work and it throwed the same exception. After much deliberation, I founded out that (maybe) Hibernate.initialize(...) only works if I call it in the initial transaction, the one which fetched me my user in the first place. Meaning, it's of no use to me since my use-case is very user-driven. I ONLY want to get this when my user asks for it, and this is always going to be in some other part of my application outside of the parent transaction.
I am looking for solutions other than the following:
Changing the child resolver to something like this:
#DgsData(parentType = "User", field = "articles")
#Transactional
public List<Article> getArticle(DgsDataFetchingEnvironment dfe) {
User user = dfe.getSource();
List<Article> articles = articlesRepository.getArticlesByUserId(user.getUserId);
return articles;
}
I am not in the favor of the above solution since I feel this is under-utilizing the ORM itself by trying to resolve the relation yourself rather than letting hibernate itself do it. (Correct me if I wrong thinking this way)
Changing my User entity to use FetchMode.JOIN.
#Entity
public class User {
#Id
#GeneratedValue
private UUID id;
#OneToMany(mappedBy = "user", fetch = FetchType.LAZY)
#Fetch(FetchMode.JOIN)
private List<Article> articles;
...
}
This is the same as telling hibernate to eagerly load the below collection no matter what. I don't want this either.
Setting spring.jpa.open-in-view=false to spring.jpa.open-in-view=true. Not in the favor of this either since this is just a band aid for LazyInitializationExceptions.
Any other solutions that just makes your forget about LazyInitializationException by keeping the session open throughout the lifecycle of the request.
Please note this answers assumes that Spring Data JPA can be used.
Helpful can be full dynamic usage of EntityGraphs
Entity Graphs give us a possibility to define fetch plans and declare which
relations (attributes) have to be queried from the database.
According to the documentation
You can do something similar to this
productRepository.findById(1L, EntityGraphUtils.fromAttributePaths(“article, “comments”));
And pass all necessary params (relations) based on user selection to the EntityGraphUtils.fromAttributePaths method.
This give us possibility to fetch only necessary data.
Additional resources:
Sample project
Spring Blog mentioned this extension
JPA EntityGraph
EntityGraph
Another workaround I've used is to skip any child resolver and just load additional entities conditionally in the base resolver.
#DgsQuery
public User getUserById(#InputArgument UUID id) {
var user = userService.findById(id);
if (dfe.getSelectionSet().contains("articles") {
Hibernate.initialize(user.getArticles());
}
return user;
}
I am building an API to return two fields as such:
{
currentPoints: 325,
badgeName: "Some Badge"
}
However, I am having trouble using hibernate in order populate those two fields. I made two attempts and both are throwing errors. Both of these errors can be found in their respective Repository file. In the 2nd attempt, I am using native=true and am able to get it to work using a SELECT *. However, I am trying to only populate and return two fields of the entity.
One solution I thought about is using the 2nd approach with a SELECT * and creating another package named response with CurrentInfoResponse class and just returning that class. However, I wanted to see if there was a way to avoid this using the current model that I have.
Possible Solution:
#Getter
#AllArgsConstructor
public class CurrentInfoResponse{
private Integer currentPoints;
private String badgeName
}
Package Structure:
Controller.java:
#GetMapping("/current-badge/{userId}")
public CurrentBadgeInfoModel getCurrentBadge(#PathVariable Integer userId){
return currentBadgeInfoService.getCurrentBadge(userId);
}
ServiceImpl.java:
#Override
public CurrentBadgeInfoModel getCurrentBadge(Integer userId){
return currentBadgeInfoRepository.getCurrentBadge(userId);
}
CurrentBadgeInfoModel.java:
#Getter
#Entity
#Table(name = "user_current_badge_info")
public class CurrentBadgeInfoModel {
#Id
#Column(name = "user_current_info_id")
private Integer userCurrentBadgeInfo;
#Column(name = "user_id")
private Integer userId;
#Column(name = "current_points")
private Integer currentPoints;
#ManyToOne
#JoinColumn(name = "badge_id")
private BadgeModel badgeModel;
}
BadgeModel.java
#Getter
#Entity
#Table(name = "badge_info")
public class BadgeModel {
#Id
#JoinColumn(name= "badge_id")
private Integer badgeId;
#Column(name = "badge_name")
private String badgeName;
}
Repository.java - ATTEMPT 1:
#Repository
public interface CurrentBadgeInfoRepository extends JpaRepository<CurrentBadgeInfoModel, Integer> {
#Query("SELECT cbim.currentPoints, cbim.badgeModel.badgeName FROM CurrentBadgeInfoModel cbim JOIN
cbim.badgeModel WHERE cbim.userId=?1")
CurrentBadgeInfoModel getCurrentBadge(Integer userId);
}
//Error: No converter found capable of converting from type [java.lang.Integer] to type [com.timelogger.model.CurrentBadgeInfoModel]
Repository.java - ATTEMPT 2:
#Repository
public interface CurrentBadgeInfoRepository extends JpaRepository<CurrentBadgeInfoModel, Integer> {
#Query(value = "SELECT current_points, badge_name FROM user_current_badge_info ucbi JOIN badge_info bi ON ucbi.badge_id=bi.badge_id WHERE user_id=?1", nativeQuery = true)
CurrentBadgeInfoModel getCurrentBadge(Integer userId);
}
//Error: Column 'user_current_info_id' not found
Using the SELECT clause of HQL should help you here.
If you don't have that constructor, you can add it
#Query("SELECT new CurrentBadgeInfoModel(cbim.currentPoints, cbim.badgeModel.badgeName) FROM CurrentBadgeInfoModel cbim JOIN
cbim.badgeModel WHERE cbim.userId=?1")
Notice the usage of new CurrentBadgeInfoModel(cbim.currentPoints, cbim.badgeModel.badgeName)
I think this is a perfect use case for Blaze-Persistence Entity Views.
I created the library to allow easy mapping between JPA models and custom interface or abstract class defined models, something like Spring Data Projections on steroids. The idea is that you define your target structure(domain model) the way you like and map attributes(getters) via JPQL expressions to the entity model.
A DTO model for your use case could look like the following with Blaze-Persistence Entity-Views:
#EntityView(CurrentBadgeInfoModel.class)
public interface CurrentInfoResponse {
Integer getCurrentPoints();
#Mapping("badgeModel.badgeName")
String getBadgeName();
}
The Spring Data integration allows you to use it almost like Spring Data Projections: https://persistence.blazebit.com/documentation/entity-view/manual/en_US/index.html#spring-data-features
CurrentInfoResponse findByUserId(Integer userId);
The best part is, it will only fetch the state that is actually necessary!
I have a User object, and a Ticket object that have a ManyToMAny relationship
class User{
private Long id;
#ManyToMany
private Set<Ticket> tickets;
}
class Ticket{
#ManyToMany
private Set<User> users;
}
Obviously this is a very simplified pseudo-like version of the code, but what would i name the method in my JPA Repository, to get all of the tickets that have a user with the specified ID in it? Is this possible, or should I make a custom query?
You can write 2 different named queries:
public interface TicketRepository extends JpaRepository<Ticket, Long> {
List<Ticket> findAllByUsers(User user);
List<Ticket> findAllByUsersIdIn(List<Long> userIds);
}
Method findAllByUsers(..) takes User object to search and return results, method findAllByUsersIdIn(..) takes user ids to search and return results.
I have this entities:
public class AnswerEntity {
#ManyToOne
private UserEntity user;
#ManyToOne
private AnswerDirectoryEntity answer;
#ManyToOne
private QuestionEntity question;
}
public class QuestionEntity {
#ManyToOne
private QuestionnaireEntity questionnaire;
}
public class QuestionnaireEntity {
private String code;
}
I need to take all user answers by user ID and corresponding code from QuestionnaireEntity.
I do it by create query like this:
List<AnswerEntity> answerList = answerRepository.findAllByUserId(userId);
and iterate over each object in my list and with using equals I compare each object to my questionnaire code:
for(AnswerEntity answerEntity : answerList){
if(answerEntity.getQuestion().getQuestionnaire().getCode().equals(questionnaireId)){
///
}
but this solution is very slow because it must iterate each object from my database,
can anybody tell my how to create an query in my repository which can help me?
You can use JPA method query this way in repository
List<AnswerEntity> findByUserIdAndQuestionQuestionnaireCode(Integer userId, String code);
I was trying to log additional user data with the revisions created by Envers. I was able to do that using RevisionEntity and RevisionListener but I'm not able to retrieve the data that is logged.
I tried the following code
AuditQuery auditQuery = AuditReaderFactory
.get(factory.getCurrentSession()).createQuery()
.forRevisionsOfEntity(Currency.class, false, false)
.add(AuditEntity.id().eq("ENV_US"));
List<Object[]> l = auditQuery.getResultList();
This returned a List
In the object array first element is the Revision Second is of RevisionEntity and third is of RevisionType, but the values in RevisionEntity object are all null.
Here is the pojo for RevisionEntity
#Entity
#Table(name = "REVINFO")
#RevisionEntity(RevListener.class)
public class ExampleRevEntity {
#Id
#GeneratedValue
#RevisionNumber
#Column(name = "REV")
private int rev;
#RevisionTimestamp
#Column(name = "REVTSTMP")
private long revtstmp;
#Column(name = "USERID")
private String userId;
public String getUserId() {
return userId;
}
public void setUserId(String userId) {
this.userId = userId;
}
}
Please let me know If I'm doing any thing wrong.
You may need to actually use the object. Hibernate/Envers will return a lazy initialized object, and debuggers will probably not be able to see the values. Once you call the getters in code, the proper values should be populated.
Do you want to query the revision entity itself, or retrieve audited objects including the revision entity?
If you want to query the revision entity itself, it's a completely normal entity. Just query it as all other entities - not through an AuditQuery, but using an EntityManager or Session.
If you want to retrieve audited objects including the revision entity, then the above is correct, provided that there exist revision data for the revisions at which the object changed. Do you see data in the database corresponding to the returned revisions?