pals.
I have an issue with Hibernate's JPA implementation. I use spring-boot-starter-data-jpa and PostgreSql v9.
I have two entities with bidirectional connection via OneToMany & ManyToOne:
#Entity
public class ShoppingCart {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#Column(name = "name")
private String name;
#OneToMany(mappedBy = "shoppingCart", cascade = {CascadeType.ALL})
private List<Good> goods = new ArrayList<>();
public void addGood(Good good) {
good.setShoppingCart(this);
goods.add(good);
}
public Good removeGood(Good good) {
goods.remove(good);
good.setShoppingCart(null);
return good;
}
public ShoppingCart() {
}
public List<Good> getGoods() {
return goods;
}
public ShoppingCart(String name) {
this.name = name;
}
public Long getId() {
return id;
}
public String getName() {
return name;
}
}
And second entity is
#Entity
public class Good {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#Column(name = "name")
private String name;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "cart_id")
#JsonIgnore
private ShoppingCart shoppingCart;
public ShoppingCart getShoppingCart() {
return shoppingCart;
}
public void setShoppingCart(ShoppingCart shoppingCart) {
this.shoppingCart = shoppingCart;
}
public Good(String name) {
this.name = name;
}
public Good() {
}
public Long getId() {
return id;
}
public String getName() {
return name;
}
}
Also I use CrudRepository to access ShoppingCart
public interface ShoppingCartRepository extends CrudRepository<ShoppingCart, Long> {}
And when I'm trying to fill existing cart I have two goods in my database. This is a code to add some goods into existing cart:
ShoppingCart cart = shoppingCartRepository.findOne(id);
cart.addGood(new Good("Butter"));
return shoppingCartRepository.save(cart);
In table "good" I have now two elements with different PKey and same data
5;"Butter";100
6;"Butter";100
Why it happens?
Also, when I'm trying to insert breakpoint at repository.save line, I see only one good in goods list in cart.
So, the problem is solved.
First way to solve is to make method with save code #Transactional.
Secon way is to use getGoods() instead of goods. We should change this code
public void addGood(Good good) {
good.setShoppingCart(this);
goods.add(good);
}
public Good removeGood(Good good) {
goods.remove(good);
good.setShoppingCart(null);
return good;
}
to this
public void addGood(Good good) {
good.setShoppingCart(this);
this.getGoods().add(good);
}
public Good removeGood(Good good) {
this.getGoods().remove(good);
good.setShoppingCart(null);
return good;
}
getGoods() here forces hibernate to update state of object and everything works fine.
As for me, I use both ways together
It happens because you create a new Good object without id. So Hibernate will generate a new id and persist the new object. If you don't want to create a new object, but only assign an already existing one, you either have to fetch the existing one from the database and assign it to the ShoppingCart oder add the ID if you create the new Good object.
Related
I have a problem with my Club entity - I'm using LAZY fetch type and ModelMapper to return my JSON. The problem is that if I use LAZY instead of EAGER what I get as a response of GET /api/players/{id} is:
Resolved [org.springframework.http.converter.HttpMessageNotWritableException: Could not write JSON: could not initialize proxy
and a screenshot from Postman:
When I debug my controller's action:
#GetMapping("/api/players/{id}")
ResponseEntity<PlayerDto> getPlayer(#PathVariable String id) {
Player foundPlayer = playerInterface.getPlayer(Long.valueOf(id));
PlayerDto playerToDto = convertToDto(foundPlayer);
return ResponseEntity.ok().body(playerToDto);
}
...
private PlayerDto convertToDto(Player player) {
return modelMapper.map(player, PlayerDto.class);
}
it seems like both foundPlayer and playerToDto have the Club like this:
but when I do foundPlayer.getClub().getName() I get a proper name. I know it's probably expected behavior, but I would love to have the Club returned in my response like this (screenshot from the response if EAGER is set):
without having to set the fetch type to EAGER.
My Player entity:
#Entity
public class Player {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String firstName;
private String lastName;;
#ManyToOne(cascade = { CascadeType.PERSIST, CascadeType.REMOVE }, fetch = FetchType.EAGER)
#JsonManagedReference
private Club club;
My Club entity:
#Entity
public class Club {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
#OneToMany(mappedBy = "club", cascade = CascadeType.PERSIST, fetch = FetchType.LAZY)
#JsonBackReference
private List<Player> players;
getPlayer method from the PlayerService (the one, that the controller calls):
#Override
public Player getPlayer(Long id) {
Optional<Player> foundPlayer = playerRepository.findById(id);
return foundPlayer.orElseThrow(PlayerNotFoundException::new);
}
PlayerToDto:
package pl.ug.kchelstowski.ap.lab06.dto;
import pl.ug.kchelstowski.ap.lab06.domain.Club;
public class PlayerDto {
private Long id;
private String firstName;
private String lastName;
private Club club;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
public Club getClub() {
return club;
}
public void setClub(Club club) {
this.club = club;
}
}
You're right, this is the expected behavior of lazy loading. It's a good thing, don't set it to eager! Instead of returning a Club #Entity class directly on your response body, you should create a ClubDto and initialize it with another convertToDto method. It's kinda tedious (I like using Mapstruct and Lombok to alleviate that), but it'll induce Hibernate to make all the queries you need.
#Data
public class ClubDto {
private String id;
private String name;
}
#Mapper
public interface ClubMapper {
public ClubDTO mapToDto(Club club);
}
Oops, didn't realize you were already using ModelMapper. I'm not too familiar with that, but it sounds like it will just work if you swap Club for ClubDto.
I have a solution guys, but I'd like to hear from you if it can be done this way, or it is some kind of anti-pattern.
I just simply set the playerToDto's Club to the brandly new fetched Club with the ID of the foundPlayer
#GetMapping("/api/players/{id}")
ResponseEntity<PlayerDto> getPlayer(#PathVariable String id) {
Player foundPlayer = playerInterface.getPlayer(Long.valueOf(id));
PlayerDto playerToDto = convertToDto(foundPlayer);
playerToDto.setClub(clubInterface.getClub(foundPlayer.getClub().getId()));
return ResponseEntity.ok().body(playerToDto);
}
In the end I came up with this:
#GetMapping("/api/players")
ResponseEntity<List<PlayerDto>> getAllPlayers() {
List<PlayerDto> playersList = playerInterface.getAllPlayers().stream().map(this::convertToDto).collect(Collectors.toList());
playersList.forEach(playerInterface::fetchClubToPlayer);
return ResponseEntity.ok().body(playersList);
}
#GetMapping("/api/players/{id}")
ResponseEntity<PlayerDto> getPlayer(#PathVariable String id) {
Player foundPlayer = playerInterface.getPlayer(Long.valueOf(id));
PlayerDto playerToDto = convertToDto(foundPlayer);
playerInterface.fetchClubToPlayer(playerToDto);
return ResponseEntity.ok().body(playerToDto);
}
public PlayerDto fetchClubToPlayer(PlayerDto player) {
if (player.getClub() != null) {
Club club = clubInterface.getClub(player.getClub().getId());
player.setClub(club);
}
return player;
}
is it fine?
I suggest you use #EntityGraph to configure the fetch plan of the resulting method's query. For example, you can declare a method in PlayerRepository to find a Player entity by id, apart from the default findById method, where its Club entity would be fetched eagerly.
public interface PlayerRepository extends JpaRepository<Player, Long>{
...
#EntityGraph(attributePaths = {"club"})
Optional<Player> findWithClubFetchedEagerlyById(LongId);
}
By providing attributePaths, fields that should be fetched eagerly are defined.
If the Club entity should be always fetched eagerly when you call the findById method then there's no need for a separate method, therefore you can annotate the default one with #EntityGraph.
With this solution, the network traversal is minimized because all needed data is fetched at once from a database.
I have defined customer entity
#Entity
#Table(name = "customer")
public class Customer {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#Column(name = "id")
private Long id;
#Column(name = "name")
private String name;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
and CrudRepository
public interface CustomerRepo extends CrudRepository<Customer, Long> {
}
if I use CustomerRepo.findById method for finding Customer
#Autowired
CustomerRepo repo;
Optional<Customer> dbCustomer = repo.findById(id);
how can I get name of that customer. I cannot use getter then.
so I'm interested is there any solution of using getters of Optional, or I need to use other method for finding Customer by id?
Optional<Customer> is returned, because it is not guaranteed that there will be such a customer with the requested ID in the database.
Instead of returning null it simply means that Optional.isPresent() will return false when the ID does not exist.
According to the API Docs (https://docs.spring.io/spring-data/commons/docs/current/api/org/springframework/data/repository/CrudRepository.html#findById-ID-):
Returns:
the entity with the given id or Optional#empty() if none found
You will therefore probably want to simply use the methods on Optional to check whether it contains a Customer (i.e. a Customer with that ID exists), and then get the name like so:
Optional<Customer> dbCustomer = repo.findById(id);
if(dbCustomer.isPresent()) {
Customer existingCustomer = dbCustomer.get();
String nameWeWanted = existingCustomer.getName();
//operate on existingCustomer
} else {
//there is no Customer in the repo with 'id'
}
Alternatively you can try callback style (shown with Java 8 Lambda):
Optional<Customer> dbCustomer = repo.findById(id);
dbCustomer.ifPresent(existingCustomer -> {
String nameWeWanted = existingCustomer.getName();
//operate on existingCustomer
});
It is worth noting that it is possible to check existence of the ID without actually retrieving/loading the entity by using the interface method:
boolean CrudRepository.existsById(ID id)
This saves an entity load, but it still requires a database roundtrip.
Try to use another method for finding Customer:
#Autowired
CustomerRepo repo;
Customer dbCustomer = repo.findOne(id);
I have a spring/jpa project that uses a mysql database. I have updated an already existent entity so that it has a new #OneToMany relationship with a new table that I have created (Which has an #ManyToOne annoation). I have gotten this to work with an embedded database with unit tests and is working properly. But when I try and deploy it to my development server, it seems that the mapping is not registered, and the data just returns a list with size 0 when it should return more.
The function that I call is
public List<Recommendation> getRecommendationsByEmployeeId()
I belive that this issue is related to some JPA magic that is done when creating a database, that does not occur when deployed against an already existent one. Is this right to assume? Or Why would this work when running my tests against an embedded database but not against the already existent development database?
These are the relevant entities.
#Entity
#Table(name = "recommendation")
public class Recommendation extends BusinessEntity {
#ManyToOne
#JoinColumn(name="employeeid")
#JsonBackReference
//This is simply to avoid a stackoverflow error
private Employee employee;
//New relationship
#OneToMany(mappedBy = "recommendation", fetch = FetchType.EAGER, cascade = CascadeType.ALL)
#JsonBackReference
List<RecommendationImage> recommendationimages;
public Recommendation(){}
public Recommendation(Employee employee, String title, String description, double targetLat, double targetLong,
String street, String city, String country
) {
this.employee = employee;
this.title = title;
this.description = description;
this.targetLat = targetLat;
this.targetLong = targetLong;
this.street = street;
this.city = city;
this.country = country;
this.active = true;
}
public Employee getEmployee() {
return employee;
}
public void setEmployee(Employee employee) {
this.employee = employee;
}
public List<RecommendationImage> getRecommendationimages() {
return recommendationimages;
}
public void setRecommendationimages(List<RecommendationImage> recommendationimages) {
this.recommendationimages = recommendationimages;
}
}
New entity
#Entity
#Table(name = "recommendationimage")
public class RecommendationImage extends ImageEntity{
#ManyToOne
#JoinColumn(name="recommendationid")
private Recommendation recommendation;
public RecommendationImage(){}
public RecommendationImage(Recommendation recommendation, String path){
this.recommendation = recommendation;
this.path = path;
}
public Recommendation getRecommendation() {
return recommendation;
}
public void setRecommendation(Recommendation recommendation) {
this.recommendation = recommendation;
}
}
Any help with this issue would be great! Thanks!
I am doing first step in Hibernate. I have a task I cannot solve.
I have two tables and want to declare connection between them by finding the same String in two tables and assigning the corresponding id.
Table 1 contains list of systems.
#Entity(name="system")
public class SystemDsc {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
And Table 2 contains list of orders.
#Entity(name="system_contract")
public class Order {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
private long system_id;
private String system;
public long getSystem_id() {
return system_id;
}
public void setSystem_id(long system_id) {
this.system_id = system_id;
}
public String getSystem() {
return system;
}
public void setSystem(String system) {
this.system = system;
}
}
Multiple orders can have the same system value(for axample A01, B07 etc).
List of systems with corresponding id values is stored in system table.
While adding a new order, hibernate should look the same name of the system in system table and save corresponding to it id value in system_id column.
Can it be done by #JoinColumn #OneToMany annotation?
I did a #ManyToMany relationship in Hibernate with an extra column successfully, as follows.
Activity.class
#Entity
#Table(name = "Activity")
public class Activity {
#Id
#GeneratedValue
private int actId;
#OneToMany(fetch = FetchType.EAGER, mappedBy = "pk.activity",
cascade = { CascadeType.ALL, CascadeType.REMOVE })
private Set<ActivityRepairMap> activityRepairMaps = new HashSet<ActivityRepairMap>();
#NotEmpty
private String actTurno;
#NotEmpty
private String actTexto;
private String actFhc;
public Activity() {
}
// Getters and Setters
}
Repair.class
#Entity
#Table(name = "Repair2")
public class Repair {
#Id
#GeneratedValue
private int repId;
#OneToMany(fetch = FetchType.EAGER, mappedBy = "pk.repair")
private Set<ActivityRepairMap> activityRepairMaps = new HashSet<ActivityRepairMap>();
#NotEmpty(message=Constants.EMPTY_FIELD)
private String repNombre;
private Integer repCant;
public Repair() {
}
// Getters and Setters
}
ActivityRepairMap.class
#Entity
#Table(name="ActivityRepairMap")
#AssociationOverrides({
#AssociationOverride(name="pk.activity", joinColumns=#JoinColumn(name="actId")),
#AssociationOverride(name="pk.repair", joinColumns=#JoinColumn(name="repId"))
})
public class ActivityRepairMap {
private ActivityRepairId pk = new ActivityRepairId();
private Integer actRepCant;
#EmbeddedId
public ActivityRepairId getPk() {
return pk;
}
public void setPk(ActivityRepairId pk) {
this.pk = pk;
}
#Transient
public Activity getActivity() {
return getPk().getActivity();
}
public void setActivity(Activity activity) {
getPk().setActivity(activity);
}
#Transient
public Repair getRepair() {
return getPk().getRepair();
}
public void setRepair(Repair repair) {
getPk().setRepair(repair);
}
#Column(name="actRepCant")
public Integer getActRepCant() {
return actRepCant;
}
public void setActRepCant(Integer actRepCant) {
this.actRepCant = actRepCant;
}
public ActivityRepairMap (){
}
// hashCode and equals methods
}
ActivityRepairId
#Embeddable
public class ActivityRepairId implements Serializable{
private static final long serialVersionUID = -776429030880521951L;
private Activity activity;
private Repair repair;
#ManyToOne
public Activity getActivity() {
return activity;
}
public void setActivity(Activity activity) {
this.activity = activity;
}
#ManyToOne
public Repair getRepair() {
return repair;
}
public void setRepair(Repair repair) {
this.repair = repair;
}
// hashCode and equals method
}
My problem is that I can't query all the repairs used in a specific activity.
I've already checked in MySQL Workbench that the data stored in the DB is correct.
I would appreciate if anyone can explain me either using HQL or Criteria how can I achieve this.
Thanks a lot.
In SQL this should be:
SELECT
r.*
FROM
repair r
LEFT JOIN
activity_repair ar
ON
ar.repair_id = r.id
WHERE
ar.activity_id = ?
Now it's still possible that a single activity is connected with two repairs, and though you might get some repairs twice in the result list. You could simple use a SELECT DISTINCT r.* to work around this, or work with a subquery.
In JPQL the query should be bascially the same as the SQL from above.
SELECT
r
FROM
Repair r
WHERE
r.activityRepairMaps.pk.activity = ?
If you need a JOIN:
SELECT
r
FROM
Repair r
JOIN
ActivityRepairMap arm
WHERE
arm.pk.activity = ?
Maybe you need to use #MapsId within your ActivityRepairMaps class. (I haven't done JPQL for a while now)
As a far as I remember, you should NOT use Entities within your #EmbeddedId classes, but instead use the raw #Id type of the corresponding classes. Instead of Repair and Activity, you should use int and int.