Spring Data JPA Many to Many Service Repository Problem - java

I am trying to add ManyToMany entity to my application. I created entity but cannot implement it.
Actor class
#Entity
#Table(name = "actor")
public class Actor {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
#Column(nullable = false, name = "actor_name")
private String actorName;
#ManyToMany(mappedBy = "actor", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
private Set<Movie> movie = new HashSet<Movie>();
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getActorName() { return actorName; }
public void setActorName(String actorName) {
this.actorName = actorName;
}
public Set<Movie> getMovie() {
return movie;
}
public void setMovie(Set<Movie> movie) {
this.movie = movie;
}
}
In movie class I have
#ManyToMany(cascade = CascadeType.ALL)
#JoinTable(
name = "movie_actor",
joinColumns = {#JoinColumn(name = "movie_id")},
inverseJoinColumns = {#JoinColumn(name = "actor_id")}
)
Set<Actor> actor = new HashSet<Actor>();
........................
public Set<Actor> getActor () {
return actor;
}
public void setActor(Set<Actor> actor){
this.actor = actor;
}
I created my entity just like this but in MovieService;
Actor actor = ActorRepository.findByActorName(movie.getActor().getActorName());
movie.setActor(actor);
This part gives me error. movie.getActor().getActorName() method cannot find. Where do I need to look? In IDE it also says method getActorName and setActorName is never used. I am also adding my ActorRepository and ActorService to closer look to the problem.
ActorRepository
public interface ActorRepository extends JpaRepository<Actor, Integer> {
Set<Actor> findByActorName(String actorName);
}
ActorService
#Service
public class ActorService {
private ActorRepository actorRepository;
#Autowired
public ActorService(ActorRepository actorRepository) {
this.actorRepository = actorRepository;
}
public List<Actor> getAllActor() {
return actorRepository.findAll();
}
}
After adding ManyToMany I was using is as OneToMany entity. Services is works for OneToMany. How can I use them for ManyToMany? I need to add multiple actors to my movies. I couldn't find MVC projects for ManyToMany implementation.

You're invoking movie.getActor().getActorName() which basically does a getActorName() on a Set<Actor> object.
You're basically treating the relation as a ManyToOne instead of a OneToMany
You could use the following to fetch the first Actor of the Set
ActorRepository.findByActorName(movie.getActors().iterator().next().getActorName());
But then of course, you don't have all your Actor's names
What you could do is the following
public interface ActorRepository extends JpaRepository<Actor, Integer> {
Set<Actor> findByActorNameIn(List<String> actorName);
}
And invoke it that way
ActorRepository.findByActorNameIn(
movie.getActors()
.stream()
.map(Actor::getName)
.collect(Collectors.toList())
);

Related

Java Spring Reactive, returning one Mono<..> from many multiple requests

[Java, Spring Reactive, MongoDB]
I'm currently trying to learn Reactive programming by doing and I found a challenge.
I have db object CategoryDB which looks like this:
#NoArgsConstructor
#Getter
#Setter
#Document(collection = DBConstraints.CATEGORY_COLLECTION_NAME)
class CategoryDB {
#Id
private String id;
private String name;
private String details = "";
#Version
private Long version;
private String parentCategoryId;
private Set<String> childCategoriesIds = new HashSet<>();
}
In a service layer I want to use model object Category.
#Getter
#Builder
public class Category {
private String id;
private String name;
private String details;
private Long version;
private Category parentCategory;
#Builder.Default
private Set<Category> childCategories = new HashSet<>();
}
I want to create Service with method Mono<Category getById(String id). In this case I want to fetch just one level of childCategories and direct parent Category. By default repository deliver Mono findById(..) and Flux findAllById(..) which I could use, but I'm not sure what would be the best way to receive expected result. I would be grateful for either working example or directions where can I find solution for this problem.
I've spent some time to figure out solution for this problem, but as I'm learning I don't know if it's good way of solving problems.
Added some methods to Category:
#Getter
#Builder
public class Category {
private String id;
private String name;
private String details;
private Long version;
private Category parentCategory;
#Builder.Default
private Set<Category> childCategories = new HashSet<>();
public void addChildCategory(Category childCategory) {
childCategory.updateParentCategory(this);
this.childCategories.add(childCategory);
}
public void updateParentCategory(Category parentCategory) {
this.parentCategory = parentCategory;
}
}
Function inside service would look like this:
#Override
public Mono<Category> findById(String id) {
return categoryRepository.findById(id).flatMap(
categoryDB -> {
Category category = CategoryDBMapper.INSTANCE.toDomain(categoryDB);
Mono<CategoryDB> parentCategoryMono;
if(!categoryDB.getParentCategoryId().isBlank()){
parentCategoryMono = categoryRepository.findById(categoryDB.getParentCategoryId());
}
else {
parentCategoryMono = Mono.empty();
}
Mono<List<CategoryDB>> childCategoriesMono = categoryRepository.findAllById(categoryDB.getChildCategoriesIds()).collectList();
return Mono.zip(parentCategoryMono, childCategoriesMono, (parentCategoryDB, childCategoriesDB) -> {
Category parentCategory = CategoryDBMapper.INSTANCE.toDomain(parentCategoryDB);
category.updateParentCategory(parentCategory);
childCategoriesDB.forEach(childCategoryDB -> {
Category childCategory = CategoryDBMapper.INSTANCE.toDomain(childCategoryDB);
category.addChildCategory(childCategory);
});
return category;
});
}
);
}
Where mapper is used for just basic properties:
#Mapper
interface CategoryDBMapper {
CategoryDBMapper INSTANCE = Mappers.getMapper(CategoryDBMapper.class);
#Mappings({
#Mapping(target = "parentCategoryId", source = "parentCategory.id"),
#Mapping(target = "childCategoriesIds", ignore = true)
})
CategoryDB toDb(Category category);
#Mappings({
#Mapping(target = "parentCategory", ignore = true),
#Mapping(target = "childCategories", ignore = true)
})
Category toDomain(CategoryDB categoryDB);
}
As I said I don't know if it's correct way of solving the problem, but it seem to work. I would be grateful for review and directions.

Spring-boot JPA infinite loop many to many

I have two entities, which are in a many to many relationship.
#Entity
public class Room {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private long id;
#ManyToMany(mappedBy = "rooms")
private Set<Team> teams;
}
#Entity
public class Team {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private long id;
#ManyToMany
#JoinTable(name = "teams_rooms",
joinColumns = #JoinColumn(name= "team_id"),
inverseJoinColumns = #JoinColumn(name = "room_id"))
private Set<Room> rooms;
}
To yield data, i have a repository for "Room" and "Team":
public interface RoomRepository extends CrudRepository<Room, Long> {
}
public interface TeamRepository extends CrudRepository<Team, Long> {
}
My goal is to request all rooms of a team, but prevent JPA from looping infinitely.
#RestController
#RequestMapping("....")
public class RoomController {
#Autowired
private RoomRepository roomRepository;
#GetMapping
public Iterable<Room> getAllRoomsOfTeam() {
final long exampleId = 1; //This is just a placeholder. The id will be passed as a parameter.
final var team = teamRepository.findById(exampleId);
return ResponseEntity.ok(team);
}
}
This is the result:
{
"id": 1,
"name": "Team1",
"rooms": [
{
"id": 1,
"name": "Room 1",
"teams": [
{
"id": 1,
"name": "Team 1",
"rooms": [
{
"id": 1,
"name": "Room 1",
"teams": [
Jackson will loop forever, until an exception occurs (Since the back reference also references the parent element, which will create a loop).
I already tried #JsonManagedReference and #JsonBackReference, but they are used for many to one relationships.
How do i stop Jackson from looping infinitely? I want to affect other repositories and queries as little as possible.
Your controller shoud not return entities ( classes with the annotation #Entity). As a best practice is to create another separate class with same attributes. This code has a little dupplication but it keeps all the layers clean. I also suggest to use #Service.
public class RoomDTO {
private String name;
private List<TeamDTO> teams = new ArrayList<>();
public RoomDTO() {
}
public RoomDTO(Room room) {
this.name = room.name;
for(Team team : room.getTeams()) {
TeamDTO teamDTO = new TeamDTO();
teamDTO.setName(team.getName);
teams.add(teamDTO);
}
}
}
public class TeamDTO {
List<RoomDTO> rooms = new ArrayList();
public TeamDTO() {
}
public TeamDTO(Team team) {
this.name = team.name;
for(Room room : team.getRooms()) {
RoomDTO roomDTO = new RoomDTO();
roomDTO.setName(team.getName);
rooms.add(roomDTO);
}
}
}
The controller should return this
#GetMapping
public Iterable<TeamDTO> getAllRoomsOfTeam() {
final long exampleId = 1;
final var team = teamRepository.findById(exampleId);
TeamDTO teamDTO = new TeamDTO(team);
return ResponseEntity.ok(teamDTO);
}
How to use DTOs in the Controller, Service and Repository pattern
Currently, there is cyclic dependency in your classes which is causing issues while converting objects to JSON. Please add #JsonIgnore annotation on rooms variable in your Team class as shown in below example:
import com.fasterxml.jackson.annotation.JsonIgnore;
#Entity
public class Team {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private long id;
#ManyToMany
#JoinTable(name = "teams_rooms",
joinColumns = #JoinColumn(name= "team_id"),
inverseJoinColumns = #JoinColumn(name = "room_id"))
#JsonIgnore
private Set<Room> rooms;
}
If you need a solution for bidirectional conversion then you can use JsonView annotation.
First of all you need to create JSON view profiles for Team and Room as shown in below example:
public class JsonViewProfiles
{
/**
* This profile will be used while converting Team object to JSON
*/
public static class Team {}
/**
* This profile will be used while converting Room object to JSON
*/
public static class Room {}
}
Use above created JSON view profiles in your entities as shown in below example:
public class Room {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#JsonView({ JsonViewProfiles.Team.class, JsonViewProfiles.Room.class })
private long id;
#JsonView(JsonViewProfiles.Room.class)
#ManyToMany(mappedBy = "rooms")
private Set<Team> teams;
}
public class Team {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#JsonView({JsonViewProfiles.Team.class, JsonViewProfiles.Room.class})
private long id;
#ManyToMany
#JoinTable(name = "teams_rooms",
joinColumns = #JoinColumn(name= "team_id"),
inverseJoinColumns = #JoinColumn(name = "room_id"))
#JsonView(JsonViewProfiles.Team.class)
private Set<Room> rooms;
}
While converting your object to JSON please use these profiles as shown in below example:
#GetMapping
public String getAllRoomsOfTeam() {
final long exampleId = 1; //This is just a placeholder. The id will be passed as a parameter.
final Team team = teamRepository.findById(exampleId);
String result = new ObjectMapper().writerWithView(JsonViewProfiles.Team.class)
.writeValueAsString(team);
return result;
}

Avoid multiple delete statements with Spring Data JPA on ManyToMany

I have some entities that are related with a #ManyToMany like this (irrelevant fields, constructors and methods removed for brevity):
#Entity
public class Profile {
#Id
#GeneratedValue
private Long id;
#ManyToMany(cascade = {
CascadeType.PERSIST,
CascadeType.MERGE
})
#JoinTable(
name = "profile_skills",
joinColumns = #JoinColumn(name = "profile_id", referencedColumnName = "id"),
inverseJoinColumns = #JoinColumn(name = "skill_id", referencedColumnName = "id")
)
private Set<Skill> skills;
public void addSkill(Skill skill) {
this.skills.add(requireNonNull(skill));
}
public Set<Skill> getSkills() {
return unmodifiableSet(skills);
}
public void removeSkill(Skill skill) {
this.skills.remove(skill);
}
}
#Entity
#EqualsAndHashCode
public class Skill {
#Id
private String id;
#Column(nullable = false)
private String name;
public String getId() {
return id;
}
}
And in a service class I'm updating Profile with new Skills like this:
#Service
#Transactional
public class ProfileUpdaterService implements ProfileUpdater {
private final ProfileRepository profileRepository;
private final SkillRepository skillRepository;
ProfileUpdaterService(ProfileRepository profileRepository, SkillRepository skillRepository) {
this.profileRepository = profileRepository;
this.skillRepository = skillRepository;
}
#Override
public WebResponse updateData(Supplier<User> userSupplier, ProfileRequest request) {
Profile profile = profileRepository.findByOwner(userSupplier.get())
.map(updateProfileValues(request))
.orElseGet(() -> makeProfile(userSupplier, request));
Profile save = profileRepository.save(profile);
return save != null ? () -> OperationResult.PROFILE_UPDATED : () -> OperationResult.OPERATION_FAILED;
}
private Function<Profile, Profile> updateProfileValues(ProfileRequest request) {
return profile -> {
updateSkills(request, profile);
return profile;
};
}
private void updateSkills(ProfileRequest request, Profile profile) {
Set<Skill> requestSkills = extractSkillsFromRequest(request).collect(toSet());
requestSkills.forEach(profile::addSkill);
Set<Skill> removedSkills = profile.getSkills().stream()
.filter(skill -> !requestSkills.contains(skill))
.collect(toSet());
removedSkills.forEach(profile::removeSkill);
}
private Stream<Skill> extractSkillsFromRequest(ProfileRequest request) {
if (request.getSkills().isEmpty()) {
return Stream.empty();
}
return request.getSkills().stream()
.filter(s -> !s.isEmpty())
.map(this::findOrSaveSkill)
.distinct();
}
private Skill findOrSaveSkill(String s) {
return skillRepository.findById(s)
.orElseGet(() -> skillRepository.save(Skill.of(s)));
}
}
TL;DR: I get a new set of Skills from a request and I have to add the ones that are not present in the Profile and delete the ones not present in the request.
This way of doing it performs one delete statement on the join table for each skill that is removed from the Profile. What would be a (clean) way to combine this into a single delete statement?
NOTE: This is a Spring Boot 1.5.12 app with Spring Data JPA 1.11.11
The issue was, of course, my insufficient knowledge of Hibernate. This problem can be fixed easily setting Hibernate's jdbc.batch_size property. In Spring Boot, on application.properties add this line:
spring.jpa.properties.hibernate.jdbc.batch_size=20
And done, now Hibernate batches up to 20 statements.

hibernate update one to many items set

Sales Order Entity.
#Entity
#Table(name = "sales_orders")
#IdClass(ReceiptPK.class)
public class SalesOrders implements Serializable {
public SalesOrders() {
}
#Id
protected Integer receiptID;
#Id
protected Integer dateKey;
public SalesOrders(Integer receiptID, Integer dateKey) {
this.receiptID = receiptID;
this.dateKey = dateKey;
}
//order contains many details
#OneToMany(fetch = FetchType.LAZY, mappedBy = "salesOrders")
#Cascade({CascadeType.SAVE_UPDATE, CascadeType.DELETE})
private Set<SalesOrderDetails> orderDetails = new HashSet<SalesOrderDetails>();
public Set<SalesOrderDetails> getOrderDetails() {
return orderDetails;
}
public void setOrderDetails(Set<SalesOrderDetails> orderDetails) {
this.orderDetails = orderDetails;
}
// other property ..
Order Details Entity.
#Entity
#Table(name = "sales_order_details")
public class SalesOrderDetails implements Serializable {
public SalesOrderDetails() {
}
private int id;
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
// Order holder
private SalesOrders salesOrders;
#ManyToOne
#JoinColumns({
#JoinColumn(name = "order_num", referencedColumnName = "receiptID"),
#JoinColumn(name = "date_key", referencedColumnName = "dateKey")})
public SalesOrders getSalesOrders() {
return salesOrders;
}
public void setSalesOrders(SalesOrders salesOrders) {
this.salesOrders = salesOrders;
}
// other property ...
My Question : When i try to remove Order item that doesn't affect on sales_order_details .
SalesOrders saleOrder = (SalesOrders) getSession().get(SalesOrders.class ,new ReceiptPK(receiptID,dateKey));
saleOrder.getOrderDetails().remove(someDetails);
getSession().beginTransaction();
getSession().saveOrUpdate(saleOrder);
getSession().getTransaction().commit();
But someDetails doesn't removed.
-- Any help will be appreciated ...
I think that you have to commit the same transaction.
getSession().getTransaction().begin();
getSession().saveOrUpdate(saleOrder);
getSession().getTransaction().commit();
For bi-directional associations you should(must) always ensure that the associations are set correctly.
Do not allow direct access to your collections.
Provide add and remove methods for modification.
public Set<SalesOrderDetails> getOrderDetails() {
//force to use add/remove to ensure consistent object model
return Collections.unmodifiableSet(orderDetails);
}
public void addOrderDetails(SalesOrderDetails salesOrderDetails){
orderDetails.add(salesOrderDetails);
salesOrderDetails.setSalesOrders(this); //important
}
public void removeOrderDetails(SalesOrderDetails salesOrderDetails){
orderDetails.remove(salesOrderDetails);
salesOrderDetails.setSalesOrder(null); //important
}
Additionally, have you implemented equals() and hashCode() on your Entitites i.e. when you call salesOrder.getOrderDetails().remove(someDetails) is anything actually being removed from the collection?
Probably not if you have not implemented equals() and hashCode() on SalesOrderDetails.
Fianlly, you shout set the orphanRemoval flag on the OneToMany mapping to true:
http://en.wikibooks.org/wiki/Java_Persistence/Relationships#Orphan_Removal_.28JPA_2.0.29

Remote Access To Jpa Entity

I'm currently working on system that consists of Java Web app and C# client app. Web app has Java Web Service, which has method that returns entity object of Program class:
#WebMethod(operationName = "getProgram")
public Program getProgram(#WebParam(name = "macAddress") String macAddress){
Device device = DeviceManager.getInstance().getDevice(macAddress);
if(device != null){
return device.getProgram();
}
return null;
}
This return object of type Program which has many properties and relations:
#Entity
#Table(name = "PROGRAM", schema = "APP")
#XmlRootElement
#NamedQueries({
#NamedQuery(name = "Program.getProgramsByWeather", query = "SELECT p FROM Program p WHERE p.weather = :weather")})
public class Program extends DbEntity implements Serializable {
private static final long serialVersionUID = 1L;
#JoinColumn(name = "LOGO_ID", referencedColumnName = "ID")
#OneToOne(cascade = CascadeType.ALL, orphanRemoval = true, fetch= FetchType.EAGER)
private Logo logo;
#JoinColumn(name = "WEATHER_ID", referencedColumnName = "ID")
#ManyToOne
private Weather weather;
#OneToMany(cascade = CascadeType.ALL, mappedBy = "program", orphanRemoval = true)
private List<ProgramPlaylist> programPlaylistList = new ArrayList<>();
#OneToMany(cascade = CascadeType.ALL, mappedBy = "program", orphanRemoval = true)
private List<ProgramTicker> programTickerList = new ArrayList<>();
#Column(name = "UPDATED")
private boolean updated;
public Program() {
}
public Program(String name, AppUser owner) {
super(name, owner);
}
public Logo getLogo() {
return logo;
}
public void setLogo(Logo logo) {
this.logo = logo;
}
public Weather getWeather() {
return weather;
}
public void setWeather(Weather weather) {
this.weather = weather;
}
public boolean isUpdated() {
return updated;
}
public void setUpdated(boolean updated) {
this.updated = updated;
}
#XmlElement
public List<ProgramPlaylist> getProgramPlaylistList() {
return programPlaylistList;
}
#XmlElement
public List<ProgramTicker> getProgramTickerList() {
return programTickerList;
}
#Override
public String toString() {
return "Program[ id=" + getId() + " ]";
}
}
Client can get this object and accessing some properties in client app like program.name, which it inherits from DbEntity, but when i try to call something like this:
program.logo.name
client throws NullReferenceException.
Same exception occurs when i try to iterate over the elements of programPlaylistList ArrayList.
I'm assuming that the object itself that is passed through to client isn't fully loaded.
How can i solve this problem, please help?!
EDIT
Ok, so I printed out XML response that client get from service and its populated correctly, but for some reason object fields aren't populated and are mostly null.
Why is this occurring?
Bye default, the fetch strategy for #OneToMany annotations is LAZY, have you tried specifying it to EAGER like in the #oneToOne field (fetch= FetchType.EAGER)?

Categories

Resources