Spring MVP Forms overwriting Many to Many ArrayList when updating an object - spring

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);
}

Related

Spring, Question regarding Query on Many-to-Many(Association with Extra Columns) in web development

I have 3 class that using Many to Many relationship in Entity level (herbibate)
They are Teams,Users and TeamUsers.
Since I am using hibernate they are all Fetch Lazy
Team:
Users:
TeamUsers:
and we have 3 interface (using by #Autowired in Service or repo layer) which is:
1.TeamRepository extends PagingAndSortingRepository<Team, Integer>
2.UserRepository extends PagingAndSortingRepository<User, Integer>
3.TeamUserRepository extends PagingAndSortingRepository<TeamUsers, Integer>
And here is the core code for many to many relationship mapping:
Team class:
#OneToMany(mappedBy = "team", fetch = FetchType.EAGER)
private Set<TeamUsers> team_users = new HashSet<TeamUsers>();
User class:
#OneToMany(mappedBy = "user", fetch = FetchType.EAGER)
private Set<TeamUsers> team_users = new HashSet<TeamUsers>();
TeamUsers class:
#ManyToOne(fetch = FetchType.EAGER, optional = false)
#JoinColumn(name = "TEAM_ID")
private Team team;
#ManyToOne(fetch = FetchType.EAGER, optional = false)
#JoinColumn(name = "USER_ID")
private User user;
Now, my questions is, what is the best practice to play around this data?
for example:
1. I need all team information and team member from my DB
In restful controller get mapping function, what should I return? should I use TeamUser repository (or maybe one more service layer) to return findAll()? if do so the data will come as object collection of User and Team right(maybe also contain extra column in that table)? Can Json successfully contain that data?
2. I am on webpage and I want to delete a user from a team.
At this timepoint I know the TeamID and UserID from HTML, when I send the Post request to API, should I get User object by UserID, then get Team object by TeamID, then get(Query) TeamUsers object by giving User object and Team Object? or maybe just query by id(from TeamUsers on html) send to API and simply remove this TeamUsers entity?
I'm new to spring and frontend development and I am much apprecaite for your help!
I would suggest the following
If you want to return all the teams and for each team all its users, you should have a TeamService which call findAll method of Team repository this way you will get a list of Teams and for each team a set with its users. And if instead of that you want all of your users and for each one what are its teams you should do the other way, call the findAll method of User repository
Both solutions will serialize to json without problem as long as the objects have its constructor and getter and setter methods
If you want to delete only the relation between user and team you could have a method in your TeamEntity Spring Data repository like this
long deleteByTeamIdAndUserId(long teamId, long userId);
And spring will create a method to perform this action or you can write the query you like just above the method name to be more specific
#Modifying
#Query("delete from TeamUsers t where t.userId=:userId and t.teamId = :teamId")
long deleteByTeamIdAndUserId(long teamId, long userId);

LazyInitializationException when fetching #EntityGraph from Hibernate 2-nd level cache

I'm developing a Spring Boot 2.3.4 web application with Spring Data JPA.
I want to use the Hibernate 2-nd level query cache for a repository method with #EntityGraph. However, I get a LazyInitializationException when generating a Thymeleaf view in case data is already in the 2-nd level cache unless I have Spring’s Open Session In View turned on. When fetching data for the first time from the database or without the 2nd level cache everything is OK even with spring.jpa.open-in-view=false. Moreover, if I enable spring.jpa.open-in-view there is no exception when fetching data from the cache without any select to the database.
How can I make Hibernate fetch at once all the associations specified in the #EntityGraph when using Hibernate 2nd level cache?
Here is my repository method:
#org.springframework.data.jpa.repository.QueryHints({#javax.persistence.QueryHint(name = "org.hibernate.cacheable", value = "true")})
#EntityGraph(attributePaths = { "venue.city", "lineup.artist", "ticketLinks" }, type = EntityGraphType.FETCH)
Optional<Event> findEventPageViewGraphById(long id);
and part of the entity:
#Entity
#org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE)
public class Event {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
protected Long id;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "venue_id")
private Venue venue;
#OneToMany(mappedBy = "event", cascade = CascadeType.ALL, orphanRemoval = true)
#org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE)
#OrderBy("orderId")
private Set<TicketLink> ticketLinks = new LinkedHashSet<>();
#OneToMany(mappedBy = "event", cascade = CascadeType.ALL, orphanRemoval = true)
#OrderBy("orderId")
#org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE)
private Set<ArtistEvent> lineup = new LinkedHashSet<>();
}
That's a known issue. Hibernate does not check the 2nd level cache for associations when constructing "just proxies". You need to access the objects to initialize them, which will then trigger a 2nd level cache hit.
I would recommend you use a DTO approach instead. 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(Event.class)
public interface EventDto {
#IdMapping
Long getId();
VenueDto getVenue();
#MappingIndex("orderId")
List<TicketLinkDto> getTicketLinks();
#MappingIndex("orderId")
List<ArtistEventDto> getLineup();
#EntityView(Venue.class)
interface VenueDto {
#IdMapping
Long getId();
CityDto getCity();
}
#EntityView(City.class)
interface CityDto {
#IdMapping
Long getId();
String getName();
}
#EntityView(TicketLink.class)
interface TicketLinkDto {
#IdMapping
Long getId();
String getName();
}
#EntityView(ArtistEvent.class)
interface ArtistEventDto {
#IdMapping
Long getId();
ArtistDto getArtist();
}
#EntityView(Artist.class)
interface ArtistDto {
#IdMapping
Long getId();
String getName();
}
}
Querying is a matter of applying the entity view to a query, the simplest being just a query by id.
EventDto a = entityViewManager.find(entityManager, EventDto.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
Optional<EventDto> findEventPageViewGraphById(long id);
Thank you Christian for your answer. I solved the problem by initializing entities with the static method Hibernate.initialize() as described here https://vladmihalcea.com/initialize-lazy-proxies-collections-jpa-hibernate/
#Transactional(readOnly = true)
public Optional<Event> loadEventPageViewGraph(long id) {
Optional<Event> eventO = eventRepository.findEventPageViewGraphById(id);
if(eventO.isPresent()) {
Hibernate.initialize(eventO.get());
Hibernate.initialize(eventO.get().getVenue().getCity());
for (ArtistEvent artistEvent: eventO.get().getLineup()) {
Hibernate.initialize(artistEvent.getArtist());
}
Hibernate.initialize(eventO.get().getTicketLinks());
return eventO;
} else {
return Optional.empty();
}
}
Though, I agree that in general it is better to use DTO's/projections. However, with DTO's there is a problem with fetching projections that include associated collections (#OneToMany properties) as described here https://vladmihalcea.com/one-to-many-dto-projection-hibernate/. In particular in the case when we don't want to select all of the entity properties. I found that Blaze-Persistence Entity Views has a nice solution for that https://persistence.blazebit.com/documentation/1.6/entity-view/manual/en_US/#subset-basic-collection-mapping. I'll check it out.

In many to many get only id instead of the whole object

public class Role {
#ManyToMany
#JoinTable(name = "user_to_role",
joinColumns = #JoinColumn(name = "role_id"),
inverseJoinColumns = #JoinColumn(name = "user_id",referencedColumnName = "id"))
private Set<User> users;
}
public class User {
#ManyToMany
#JoinTable(name = "user_to_role",
joinColumns = #JoinColumn(name = "user_id"),
inverseJoinColumns = #JoinColumn(name = "role_id"))
private Set<Role> roles;
}
I have a many to many relationship between the two classes. When calling role.getUsers(), I want to get only the user ids, the rest of the fields should be ignored, since there will be a lot of data and I don't want to load everything, How can I achieve this?
A straightforward way to do it would be to use a Criteria query, but to use it inside an Entity, you'd have to inject an EntityManager there, which is considered a bad practice. A better solution would be to create this query in a Service.
But if you still want to do it, then your getUsers method would look something like this:
public List<User> getUsers() {
Criteria cr = entityManager.createCriteria(User.class)
.setProjection(Projections.projectionList()
.add(Projections.property("id"), "id")
.setResultTransformer(Transformers.aliasToBean(User.class));
List<User> list = cr.list();
return list;
}
If you want to restrict your list, just use a Restrictions, like so: criteria.add(Restrictions.eq("id", yourRestrictedId))
Since you have mapped the entities User and Role using #ManyToMany relationship, you need to create a DAO/Service class to implement the business logic to filter only userIds and return the same.
This cannot be handled in your Model\Entity classes as it will defy the whole concept of Object-Relational mapping.
I can create the business logic using DAO for your example if you want but you will get 10's of blogs achieving the same.
For your reference,you can check my sample project here.

Spring Boot many to many post method not updating data

My User class looks like this :
#Data
#Entity
public class User {
#Id
Long userID;
#ManyToMany(mappedBy = "admins")
private List<ClassRoom> classRooms = new ArrayList<>();
}
And my ClassRoom class like this :
#Data
#Entity
public class ClassRoom {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
Long classRoomID;
#ManyToMany
#JoinTable(name ="classroom_user",
joinColumns = #JoinColumn(name = "classroom_id"),
inverseJoinColumns = #JoinColumn(name = "user_id"))
private List<User> admins = new ArrayList<>();
}
And in my UserController class, I have :
#PostMapping("user/{id}/c")
User addClassRoom(#PathVariable Long id,#RequestBody ClassRoom newClassRoom)
{
logger.debug(repository.findById(id));
return repository.findById(id)
.map(user -> {
user.getClassRooms().add(newClassRoom);
user.setClassRooms(user.getClassRooms());
return repository.save(user);
})
.orElseGet(() -> {
return null;
});
}
And I POST and empty JSON ({}) and I see no change in my users. The Classroom or an empty Classroom doesn't get added in the User.
What is the problem here? How can I resolve this ?
user.getClassRooms().add(newClassRoom); is suffice, user.setClassRooms(user.getClassRooms()); not required.
You will have to perform cascade save operation.List all cascade types explicitly and don't use mappedBy, instead use joincolumns annotation.
Can you paste the logs, please? Is Hibernate doing any insert into your table? Has the database schema been created in the DB correctly? One thing I recommend you to do is to add a custom table name on the top of your User class, using annotations like so: #Table(name = "users"). In most SQL dialects user is a reserved keyword, hence it is recommended to always annotate User class a bit differently, so that Hibernate won't have any problems to create a table for that entity.
IMO you must find classRoom by its id from repository, if it's new, you must create a new entity and save it first. Then assign it to user and save it.
The object you receive from the post method was not created by the entity manager.
After using user.getClassRooms().add(newClassRoom);
We must use userRepository.save(user);

Best practise to model custom *relations* with Spring Boot

Whats the best practise in Spring Boot to model custom relations between two entities in projection.
My entity Participation links to Competition, User and Team.
public class Participation
{
#Id
private String id;
#NonNull
#OneToOne
private Competition competition;
#OneToOne
private Team team;
#NonNull
#OneToOne
private User user;
private String info;
}
In my project, I want to link all participating Users for a specific Team to a Competition. To achieve this, I wrote a CompetitionDTO that has a field List<User> participants that is filled by a custom CompetitionService:
public Page<CompetitionDTO> teamParticipations (Team team, Pageable pageable)
{
Page<CompetitionDTO> page = cRep.findTeamParticipation(team, pageable);
page.forEach(competition -> competition.setParticipants(pRep.findParticipants(competition, team)));
return page;
}
I don't like this approach a lot because I assume there is a more elegant way to do this with Spring.

Resources