I'm a newbie in Spring. I think it's better to explain my problem with a little example. Let's say I have two main classes: User and Group. A User can be part of more Groups and a Group, obviously, can have more Users. So the relationship between them is many-to-many. What I would like to show, is something like this (using JSTL):
<c:forEach items="${groups}" var="group">
<c:out value="${group.name}"/> (<c:out value="${fn:length(group.users)}" />):<br />
<c:forEach items="${groups.users}" var="user">
<c:out value="${user.name}"/><br />
</c:forEach><br />
</c:forEach>
Basically, the output should be something like:
Random (2):
Joe
Bloggs
Star wars (5):
Luke
Chewbacca
Darth Vader
Princess Leia
Yoda
Nintendo (3):
Super Mario
Metroid
Zelda
I initially coded it with the classic #ManyToMany annotation, using an additional table user_has_group (created and managed by JPA) and it was working perfectly.
I needed to modify the structure since I needed the user_has_group table to have the joined_date column. To achieve it, I read online that the best solution is to create another class (i.e. UserHasGroup) and add the one-to-many relationships to this class from User and group. Doing so, it's possible to add additional attributes to the UserHasGroup class (and therefore additional columns to the user_has_group table). Something like:
User:
#Entity
#Table(name = "user")
public class User
{
#Id
#GeneratedValue
#Column
private int id;
#Column
private String alias;
#OneToMany(cascade = CascadeType.ALL, mappedBy = "user")
private List<UserHasGroup> userHasGroup = new ArrayList<UserHasGroup>();
// Constructors/getters/setters
}
Group:
#Entity
#Table(name = "`group`")
public class Group
{
#Id
#GeneratedValue
#Column
private int id;
#Column
private String name;
#OneToMany(cascade = CascadeType.ALL, mappedBy = "group")
private List<UserHasGroup> userHasGroup = new ArrayList<UserHasGroup>();
// Constructors/getters/setters
}
UserHasGroup:
#Entity
#Table(name = "user_has_group")
public class UserHasGroup
{
#Id
#GeneratedValue
#Column
private int id;
#ManyToOne
#JoinColumn(name = "user_id")
private User user;
#ManyToOne
#JoinColumn(name = "group_id")
private Group group;
#Column
#Temporal(TemporalType.DATE)
private Date joinedDate;
// Constructors/getters/setters
}
So far, so good. All the tests run successfully and the functionality is maintained.
But I'm facing a problem with JSTL. In fact, with this new structure is obviously not possible to do group.users to iterate through the users.
What is the best way to reach the same functionality as before but with this new structure?
Thank you.
I don't see a reason why ${fn:length(group.userHasGroup)} should'n work.
The only problem you might come accross is some no active session exception. You can solve it either by
using "open session in view" interceptor (which somebody calls an anti-pattern)
manualy iterating through the list in your service method
eager fetching the relationship - i would be very carefull here as this can lead to many queries to database
Answer to additional question:
It should look somehow like this:
<c:forEach items="${groups}" var="group">
<c:out value="${group.name}"/> (<c:out value="${fn:length(group.userHasGroup)}" />):<br />
<c:forEach items="${groups.userHasGroup}" var="userHasGroup">
<c:out value="${userHasGroup.user.name}"/><br />
</c:forEach><br />
Related
Post entity:
public class Post {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "id")
private Long id;
#OneToMany(mappedBy = "post")
private List<PostComment> postComments;
...
}
PostComment entity:
public class PostComment {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "id")
private Long id;
#ManyToOne
#JoinColumn(name = "post_id")
private Post post;
#OneToMany(mappedBy = "postComment")
private Set<PostCommentUpvote> postCommentUpvotes;
#OneToMany(mappedBy = "postComment")
private Set<PostCommentDownvote> postCommentDownvotes;
...
}
PostCommentUpvote entity (PostCommentUpvote and PostCommentDownvote have the exact same fields - these entities act like counters)
public class PostCommentUpvote {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "id")
private Long id;
#ManyToOne
#JoinColumn(name = "post_comment_id")
private PostComment postComment;
#ManyToOne
#JoinColumn(name = "user_id")
private User user;
...
}
All relations are bi-directional as you can see from the annotations.
The goal: When a user (authenticated) upvotes/downvotes a PostComment I want to do the following:
Check if user already upvoted/downvoted the PostComment.
For this I have Post id (even though this is not needed) and PostComment id and both are indexed.
There are three possible 'states' when User up/downvotes the comment:
User hasn't yet up/downvoted that comment, so it is either new upvote or new downvote
User has already upvoted and if he upvotes again, it will remove the upvote (same with downvote)
User has already upvoted and if he downvotes, upvote is removed and new downvote is added (and vice-versa)
What would be the most optimal way of doing this? Get the PostComment by its id and then loop through the List of PostCommentUpvote/PostCommentDownvote and check the User on every iteration? Or perform a tactical SQL request, which must be faster than looping in Java? If so, what would this SQL query look like? Or any other approach to make this performant. I am open to any suggestion.
Thanks
Assuming you have the post comment id and user id, the following JPA query (or close to it) will return true if the user has upvoted on the post comment and false otherwise:
select case when count(postCommentUpvote) > 0 then 'true' else 'false'
from PostCommentUpvote postCommentUpvote
join postCommentUpvote.postComment postCommnent
where postComment.id = :postCommentId
and user.id = :userId
You would then have to perform the same query using the PostCommentDownVote entity. An alternative would be to remove the up and down vote entities, simply create a PostCommentVote entity which has a boolean attribute that indicates up or down, and helper methods isUpvote() and isDownVote() that would interpret the boolean for you. You could get everything you need with a single query that returns a PostCommentVote if the user has up or down voted and null otherwise.
You did not indicate what you want to do if the user has already commented on the post; ignore the request or update the PostComment. Either way the most optimal way of doing this would be not checking at all. Create a unique index on (user_id, post_comment_id) or drop the the id column and make a composite PK of those columns. Then just insert without checking. Use the On Conflict to either ignore or update the request. You may also want to add an Up/Down vote indicator column.
I have a simple project that has a User model, Sports team model and a Many To Many table where a user can "like" the sports team.
User
#ManyToMany(fetch = FetchType.LAZY)
#JoinTable(
name = "likes",
joinColumns = #JoinColumn(name = "user_id"),
inverseJoinColumns = #JoinColumn(name = "team_id")
)
private List<Team> teamsLiked;
Team
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#NotBlank
#Size(min=2, max=30)
private String teamName;
#NotBlank
private String city;
private String sport;
#ManyToMany(fetch = FetchType.LAZY)
#JoinTable(
name = "likes",
joinColumns = #JoinColumn(name = "team_id"),
inverseJoinColumns = #JoinColumn(name = "user_id")
)
private List<User> likers;
My problem is, when I'm using Spring MVC forms for a user to edit a team, upon submission it completely wipes out existing likes on the Team object under likers. On the edit page, I am using #ModelAttribute and pre populating the existing team object, and have tried to put the likers as a hidden attribute so the data will persist, but that throws an error. I've tried on the #PostMapping backend, to set the origin list of likers before re-saving the DB and that's not working either. Besides using Normal HTML forms to update an object, is there a way I can have the list of users who liked a team persist after updating? Thanks in advance.
What you need here is a DTO and map that onto an existing entity. 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(Team.class)
#UpdatableEntityView
public interface TeamDto {
#IdMapping
Long getId();
String getTeamName();
void setTeamName(String teamName);
String getCity();
void setCity(String city);
String getSport();
void setSport(String sport);
}
Querying is a matter of applying the entity view to a query, the simplest being just a query by id.
TeamDto a = entityViewManager.find(entityManager, TeamDto.class, id);
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
Page<TeamDto> findAll(Pageable pageable);
The best part is, it will only fetch the state that is actually necessary!
And in your case of saving data, you can use the Spring WebMvc integration
that would look something like the following:
#Transactional
#PostMapping("/teams")
void save(#RequestBody TeamDto dto){
repository.save(dto);
}
consider following model:
#Entity
#Getter
#Setter
public class Person {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#ElementCollection
#Column(name = "phone")
private List<String> phoneNumber = new LinkedList<>();
}
In front-end user should be able define order of phone numbers in this sample with drag and drop or something like that. Can you tell me what is the most effective way to handle this use case? I found solution with jpa annotation #OrderColumn which generates additional column. But I guess if I need re-order items, solution is delete all from collection and save it again with new order right? I afraid that is not very elegant solution. Can you give me your advice? Thank you.
I'm fairly new to ORM. I'm having trouble deciding how exactly I should map the following entities.
DiscussionThread
Post
User
AnonymousUser
DiscussionThread would be something similar to the ones we see in bulletin boards online. It would contain a list of Post which would be posted by User. However, I do not want the User to reveal his/her identity while posting in the DiscussionThread.
In order to achieve that I created a list of proxy usernames denoted by the entity AnonymousUser. Thus, whenever a User decides to make a Post in a DiscussionThread, he would be posting as an AnonymousUser. Any further Post made by the same User in that DiscussionThread would be linked to the same AnonymousUser.The User will have different AnonymousUser names in different DiscussionThreads. An instance of AnonymousUser may be used by two different users on two different threads.
In simpler words, there will be one AnonymousUser for one User in each DiscussionThread.
I have created the following POJO entities, but I'm stuck in how I should map them to each other.
public class AnonymousUser {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
private String name;
private String displayPicture;
//Not sure how to make relationships here
private Set<DiscussionThread> discussionThreads;
private Set<User> users;
}
public class DiscussionThread {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
private String title;
private String description;
}
public class User {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
private String email;
private String username;
}
Any help will be appreciated.
Thank you!
Well, you basically described:
Don't know if it's right or not but this is one way you could diagram and think about such problems. This is Chen's database notation in Visio.
I faced the problem when I need to partially udate data in BD.
What I have:
I have three linked entities:
Profile --(1-m)--> Person --(1-1)--> Address
Where Person -> Address is lazy relationship. It was achieved via optional=false option (that allow hibernate to use proxy).
What the problem:
I need to update Profile in such way, that I needn't pull all Addresses that linked with this profile.
When I update Profile (don't work):
profile.setPersons(persons);
session.saveOrUpdate(profile);
throws: org.springframework.dao.DataIntegrityViolationException: not null property references a null or transient value
It happens because Person->Address relationship has optional=false option
I need to do:
//for each person
Address address = requestAddressFromDB();
person.setAddress(address);
persons.add(person)
//and only then
profile.setPersons(persons);
session.saveOrUpdate(profile);
profile.setPerson(person)
But I don't want to pull all address each time I update Profile name.
What is the question:
How can I avoid obligatory Person->(not null)Address constraint to save my profile without pulling all addresses?
ADDITION:
#Entity
public class Person{
#Id
#SequenceGenerator(name = "person_sequence", sequenceName = "sq_person")
#GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "person_sequence")
#Column(name = "id")
private long personID;
#OneToOne(mappedBy="person", cascade=CascadeType.ALL, optional = false, fetch = FetchType.LAZY)
private Address address;
//.. getters, setters
}
#Entity
public class Address {
#Id
#Column(name="id", unique=true, nullable=false)
#GeneratedValue(generator="gen")
#GenericGenerator(name="gen", strategy="foreign", parameters=#Parameter(name="property", value="person"))
private long personID;
#PrimaryKeyJoinColumn
#OneToOne
private FileInfo person;
}
Modify the cascade element on the #OneToOne annotation so that the PERSIST operation is not cascaded. This may require you to manually persist updates to Address in certain areas of your code. If the cascade is not really used however no change is needed.
#OneToOne(mappedBy="person", cascade={CascadeType.MERGE, CascadeType.REMOVE, CascadeType.REFRESH}, optional = false, fetch = FetchType.LAZY)
private Adress address; //Do you know that Address is missing a 'd'?