I have a standard many to many relationship in Spring with persistence using Hibernate. Constructors and setters left out for brevity.
#Entity
#Table(name = "a")
public class A {
#ManyToMany(mappedBy = "as")
private Set<B> bs;
}
#Entity
#Table(name = "bs")
public class B {
#ManyToMany
#JoinTable(
name = "b_a",
joinColumns = #JoinColumn(name = "b_id", referencedColumnName = "id"),
inverseJoinColumns = #JoinColumn(name = "a_id", referencedColumnName = "id")
)
private Set<A> as = new HashSet<>();
}
I also have DTOs for each of these classes and I have static methods to transfer between them. Due to the many-to-many relationship, I need to implement a form of state tracker to stop the program getting caught in infinite recursion.
public final class DTOEntityMapper {
private static class StateHolder {
private List<Object> alreadySeenObjects = new ArrayList<>();
void addObject(Object o){
alreadySeenObjects.add(o);
}
boolean contains(Object o){
return alreadySeenObjects.contains(o);
}
}
public static BDTO fromBToDTO(B b) {
return fromBToDTO(b, new StateHolder());
}
private static BDTO fromBToDTO(B b, StateHolder state) {
state.addObject(b);
return BDTO.builder()
.withAs(b.getAs().stream().filter(item -> !state.contains(item)).map(e -> fromAToDTO(e, state)).collect(Collectors.toSet()))
.build();
}
}
// Note: fromAToDTO does same thing.
However when I try and test I get an assertion failure:
#Test
public void convertFromBToBDTO() {
A a = new A("a");
B b = new B("b");
b.setAs(ImmutableSet.of(a));
a.setBs(ImmutableSet.of(b));
aDTO ADTO = new ADTO("a");
bDTO BDTO = new BDTO("b");
bDTO.setAs(ImmutableSet.of(aDTO));
aDTO.setBs(ImmutableSet.of(bDTO));
BDTO expected = bDTO;
assertThat(DTOEntityMapper.fromBToDTO(b)).isEqualTo(expected);
}
Actual: BDTO{as=[ADTO{bs=}]}
Expected: BDTO{as=[ADTO{bs=b1}]}
Basically I am confused with how I deal with many-to-many relationships when converting between the data model and DTOs. Hopefully this makes sense!
Related
My application operates in a weird way. In fact when debugging I can clearly see that my objects get persisted on the DB but when in running mode, JPA does not seem to persist them. The following is a code snippet from my source code :
#Entity
#Table(name = "a", schema="myschema")
public class A implements Serializable{
#Id
#Column(name = "id", unique = true, nullable = false)
#NotNull(message = "id can not be null")
private UUID id = UUID.randomUUID();
#JsonIgnore
// #ManyToMany(fetch = FetchType.EAGER)
#ManyToMany
#JoinTable(name = "a_b", joinColumns = { #JoinColumn(name = "a_id") },
inverseJoinColumns = { #JoinColumn(name = "b_id") }
)
private List<b> blist = new ArrayList<>();
//omitted source code
}
#Entity
#Table(name = "b", schema="myschema")
public class B implements Serializable{
#Id
#Column(name = "id", unique = true, nullable = false)
#NotNull(message = "id can not be null")
private Integer id;
#ManyToMany(mappedBy = "blist")
#JsonIgnore
private List<A> alist = new ArrayList<>();
//omitted source code
}
#Service
public class MyService{
//omitted source code
public Optional<A> createCopy(A source, int bId) {
B b = bRepository.findById(bId);
A copy_ = this.copy(source);
A target = aRepository.save(copy_);
b.getAlist().add(target);
bRepository.save(b);
return Optional.of(target);
}
private A copy(A source){
A target = new A();
//copy one to one from source to target
target.setB(source.getB());
return target;
}
}
When debugging I can see that after making a call to MyService#createCopy() method, a new record is persisted in the DB within the table a_b. However when I simply run the server and then proceeds with a call to MyService#createCopy(), no additional record in a_b gets persisted.
Anyone ever encountered such an odd behavior before? And if yes how can one solve it please?
It's look like you have not set your new list again to b objet in your service, That's why you are not able to get records in within the table a_b. Change your service as below.
#Service
public class MyService{
//omitted source code
public Optional<A> createCopy(A source, int bId) {
B b = bRepository.findById(bId);
A copy_ = this.copy(source);
A target = aRepository.save(copy_);
List<A> alist= b.getAlist();// Here b.getAlist() will return a independent list and adding any item within this list will not affect the list inside the object b.
alist.add(target);
b.setAlist(alist);// setting the new list to object b.
bRepository.save(b);
return Optional.of(target);
}
Try it once and let me know if it works.
I managed to fix the problem as following (Thanks to #Ajit hint):
private A copy(A source){
A target = new A();
//copy one to one from source to target
//instead of target.setB(source.getB()); I did this
List<B> targetBlist = target.getBlist();
source.getBlist().forEach(bObj -> {
Optional<B> optB = bRepository.findById(bObj.getId());
if(optB.isPresent())
targetBlist.add(optB.get());
});
return target;
}
Now that my problem is fixed I still cannot figure out what was the actual issue. It smells like this has to do something with lazy loading. In fact the only case where the problem does not occur is when I do inspect the source#bList before proceeding to the copy(). If anyone could clarify this to me I will be thankful.
Hibernate 4.3.11
I have an issue saving the following object graph in hibernate. The Employer is being saved using the merge() method.
Employer
|_ List<EmployerProducts> employerProductsList;
|_ List<EmployerProductsPlan> employerProductsPlan;
The Employer & EmployerProducts have a auto generated pk. The EmployerProductsPlan is a composite key consisting of the EmployerProducts id and a String with the plan code.
The error occurs when there is a transient object in the EmployerProducts list that cascades to List<EmployerProductsPlan>. The 1st error that I encountered which I have been trying to get past was an internal hibernate NPE. This post here perfectly describes the issue that I am having which causes the null pointer Hibernate NullPointer on INSERTED id when persisting three levels using #Embeddable and cascade
The OP left a comment specifying what they did to resolve, but I end up with a different error when changing to the suggested mapping. After changing the mapping, I am now getting
org.hibernate.NonUniqueObjectException: A different object with the same identifier value was already associated with the session : [com.webexchange.model.EmployerProductsPlan#com.webexchange.model.EmployerProductsPlanId#c733f9bd]
Due to other library dependencies, I cannot upgrade above 4.3.x at this time. This project is using spring-boot-starter-data-jpa 1.3.3. No other work is being performed on the session other than calling merge() and passing the employer object.
Below is the mappings for each class:
Employer
#Entity
#Table(name = "employer")
#lombok.Getter
#lombok.Setter
#lombok.EqualsAndHashCode(of = {"employerNo"})
public class Employer implements java.io.Serializable {
#Id
#GeneratedValue(strategy = IDENTITY)
#Column(name = "EMPLOYER_NO", unique = true, nullable = false)
private Long employerNo;
.....
#OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY, mappedBy = "employer", orphanRemoval = true)
private List<EmployerProducts> employerProductsList = new ArrayList<>(0);
}
EmployerProducts
#Entity
#Table(name = "employer_products")
#Accessors(chain = true) // has to come before #Getter and #Setter
#lombok.Getter
#lombok.Setter
#lombok.EqualsAndHashCode(of = {"employerProductsNo"})
public class EmployerProducts implements Serializable {
#Id
#GeneratedValue(strategy = IDENTITY)
#Column(name = "employer_products_no", unique = true, nullable = false)
private Long employerProductsNo;
#ManyToOne
#JoinColumn(name = "employer_no", nullable = false)
private Employer employer;
......
#OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY, mappedBy = "employerProducts", orphanRemoval = true)
private List<EmployerProductsPlan> employerProductsPlanList = new ArrayList<>(0);
}
EmployerProductsPlan
#Accessors(chain = true) // has to come before #Getter and #Setter
#lombok.Getter
#lombok.Setter
#lombok.EqualsAndHashCode(of = {"id"})
#Entity
#Table(name="employer_products_plan")
public class EmployerProductsPlan implements Serializable {
#EmbeddedId
#AttributeOverrides({ #AttributeOverride(name = "plan", column = #Column(name = "epp_plan", nullable = false)),
#AttributeOverride(name = "employerProductsNo", column = #Column(name = "employer_products_no", nullable = false)) })
private EmployerProductsPlanId id;
#ManyToOne
#JoinColumn(name = "employer_products_no")
#MapsId("employerProductsNo")
private EmployerProducts employerProducts;
}
I am populating the employerProducts above with the same instance of the EmployerProducts object that is being saved. It is transient and has no id populated as it does not existing in the db yet.
EmployerProductsPlanId
#Accessors(chain = true) // has to come before #Getter and #Setter
#lombok.Getter
#lombok.Setter
#lombok.EqualsAndHashCode(of = {"plan", "employerProductsNo"})
#Embeddable
public class EmployerProductsPlanId implements Serializable {
private String plan;
private Long employerProductsNo;
// This was my previous mapping that was causing the internal NPE in hibernate
/* #ManyToOne
#JoinColumn(name = "employer_products_no")
private EmployerProducts employerProducts;*/
}
UPDATE:
Showing struts controller and dao. The Employer object is never loaded from the db prior to the save. Struts is creating this entire object graph from the Http request parameters.
Struts 2.5 controller
#lombok.Getter
#lombok.Setter
public class EditEmployers extends ActionHelper implements Preparable {
#Autowired
#lombok.Getter(AccessLevel.NONE)
#lombok.Setter(AccessLevel.NONE)
private IEmployerDao employerDao;
private Employer entity;
....
public String save() {
beforeSave();
boolean newRecord = getEntity().getEmployerNo() == null || getEntity().getEmployerNo() == 0;
Employer savedEmployer = newRecord ?
employerDao.create(getEntity()) :
employerDao.update(getEntity());
setEntity(savedEmployer);
return "success";
}
private void beforeSave() {
Employer emp = getEntity();
// associate this employer record with any products attached
for (EmployerProducts employerProduct : emp.getEmployerProductsList()) {
employerProduct.setEmployer(emp);
employerProduct.getEmployerProductsPlanList().forEach(x ->
x.setEmployerProducts(employerProduct));
}
// check to see if branding needs to be NULL. It will create the object from the select parameter with no id
// if a branding record has not been selected
if (emp.getBranding() != null && emp.getBranding().getBrandingNo() == null) {
emp.setBranding(null);
}
}
}
Employer DAO
#Repository
#Transactional
#Service
#Log4j
public class EmployerDao extends WebexchangeBaseDao implements IEmployerDao {
private Criteria criteria() {
return getCurrentSession().createCriteria(Employer.class);
}
#Override
#Transactional(readOnly = true)
public Employer read(Serializable id) {
return (Employer)getCurrentSession().load(Employer.class, id);
}
#Override
public Employer create(Employer employer) {
getCurrentSession().persist(employer);
return employer;
}
#Override
public Employer update(Employer employer) {
getCurrentSession().merge(employer);
return employer;
}
}
As of right now, my solution is to loop through the EmployerProducts and check for new records. I called a persist on the new ones before calling the merge() on the parent Employer. I also moved the logic I had associating all the keys into the dao instead of having it in my Struts action. Below is what my update() method in the Employer DAO now looks like
public Employer update(Employer employer) {
// associate this employer record with any products attached
for (EmployerProducts employerProduct : employer.getEmployerProductsList()) {
employerProduct.setEmployer(employer);
if (employerProduct.getEmployerProductsNo() == null) {
// The cascade down to employerProductsPlanList has issues getting the employerProductsNo
// automatically if the employerProduct does not exists yet. Persist the new employer product
// before we try to insert the new composite key in the plan
// https://stackoverflow.com/questions/54517061/hibernate-4-3-cascade-merge-through-multiple-lists-with-embeded-id
List<EmployerProductsPlan> plansToBeSaved = employerProduct.getEmployerProductsPlanList();
employerProduct.setEmployerProductsPlanList(new ArrayList<>());
getCurrentSession().persist(employerProduct);
// add the plans back in
employerProduct.setEmployerProductsPlanList(plansToBeSaved);
}
// associate the plan with the employer product
employerProduct.getEmployerProductsPlanList().forEach(x ->
x.getId().setEmployerProductsNo(employerProduct.getEmployerProductsNo())
);
}
return (Employer)getCurrentSession().merge(employer);
}
I know there are a lot of similar threads out there but i just can't figure it out from those threads on how to overcome this problem.
I have 3 classes Car, Brand, Color.
A Car has just one Brand and a list of Colors.
Brand has a List of Cars.
Color does not have any relation.
Getters, Setters, ToString and Constructors are not provided for simplicity sake.
I'm able to save objects into database and database is already populated.
--------------------------------------------------------------------------------
#Entity
#Table(catalog = "spring_project")
public class Car {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String model;
#ManyToMany(cascade=CascadeType.ALL, fetch = FetchType.LAZY)
#JoinTable( name = "car_color", catalog = "spring_project",
joinColumns = { #JoinColumn(name = "car_id") },
inverseJoinColumns = { #JoinColumn(name = "colors_id") }
)
private List<Color> colors = new ArrayList<>();
#ManyToOne(cascade=CascadeType.ALL, fetch = FetchType.LAZY)
#JoinColumn(name="brand_id", referencedColumnName="id")
private Brand brand;
--------------------------------------------------------------------------------
#Entity
#Table(catalog = "spring_project")
public class Brand {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
#OneToMany(mappedBy = "brand", fetch = FetchType.LAZY)
private List<Car> cars = new ArrayList<>();
--------------------------------------------------------------------------------
#Entity
#Table(catalog = "spring_project")
public class Color {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
--------------------------------------------------------------------------------
Everything runs just fine if i fetch like Eager, but i know it is a bad practice and it should be used Lazy loading instead. But i keep getting the LazyInitializationException.
I understand from the error that a session is required but i dont know how to provide one since im working with Spring Data JPA neither where i should declare one...
#SpringBootApplication
public class SrpingJpaApplication {
private static final Logger log =
LoggerFactory.getLogger(SrpingJpaApplication.class);
public static void main(String[] args) {
SpringApplication.run(SrpingJpaApplication.class, args);
}
#Bean
public CommandLineRunner demo(CarRepository carRepository,
ColorRepository colorRepository,
BrandRepository brandRepository) {
return (args) -> {
log.info("Reads all cars....");
for (Car c : carRepository.findAll()) {
System.out.println(c.toString());
}
};
}
}
Thank you so much.
Edited----->>>
The error is thrown on c.toString();
Error: Caused by: org.hibernate.LazyInitializationException: could not initialize
proxy [com.readiness.moita.SrpingJPA.Models.Brand#1] - no Session
The default for the #OneToMany annotation is FetchType.LAZY so your collections are loaded lazily.
In order to be able to access the collection after you've retrieved the object you need to be in a transactional context (you need an open session)
When you call:
carRepository.findAll();
internally a new session is created, the object is retrieved and as soon as the findAll method returns the session is closed.
What you should do is make sure you have an open session whenever you access the lazy collection in your Car object (which the toString does).
The simplest way is to have another service handle the car loading and annotate the showCars method with #Transactional the method is in another service because of the way AOP proxies are handled.
#Service
public CarService {
final CarRepository carRepository;
public CarService(CarRepository carRepository) {
this.carRepository = carRepository;
}
#Transactional
public void showCars(String... args) {
for (Car c : carRepository.findAll()) {
System.out.println(c.toString());
}
}
}
and then you call:
#Bean
public CommandLineRunner demo(CarService carService) {
return (args) -> service.showCars(args);
}
Because the FetchType of Brand is lazy, it will not automatically be loaded into the session with call to fetchAll(). To have it automatically load into the session, you need to:
Change
#ManyToOne(cascade=CascadeType.ALL, fetch = FetchType.LAZY)
#JoinColumn(name="brand_id", referencedColumnName="id")
private Brand brand;
to
#ManyToOne(cascade=CascadeType.ALL, fetch = FetchType.EAGER)
Ex
#ManyToOne(cascade=CascadeType.ALL, fetch = FetchType.EAGER)
#JoinColumn(name="brand_id", referencedColumnName="id")
private Brand brand;
If you do not want to set the fetch type to eager, then you need to move your call to toString to a service method Ex
#Component
public CarService implements ICarService {
#Autowired
CarRepository carRepository;
#Transactional
public void printAllCars() {
for (Car c : carRepository.findAll()) {
System.out.println(c.toString());
}
}
}
The correct way to do this however would be to write a criteria query or hql
I have a basic SpringBoot app. using Spring Initializer, JPA, embedded Tomcat, Thymeleaf template engine, and package as an executable JAR file.
I have this domain class:
Entity
#DiscriminatorValue("sebloc")
public class SeblocDevice extends Device {
/**
*
*/
private static final long serialVersionUID = 1L;
public SeblocDevice() {
super();
}
public SeblocDevice(String deviceKey, String devicePAC) {
super(deviceKey, devicePAC);
}
#OneToMany(mappedBy = "device", cascade = CascadeType.ALL, fetch = FetchType.EAGER, orphanRemoval = true)
private Set<DeviceDriver> driverDevices = new HashSet<>();
public Set<DeviceDriver> getDriverDevices() {
return driverDevices;
}
public void setDriverDevices(Set<DeviceDriver> driverDevices) {
this.driverDevices = driverDevices;
}
public void clearDriverDevices() {
for (DeviceDriver deviceDriver : deviceDrivers) {
deviceDriver.setDriver(null);
driverDevices.remove(deviceDriver);
}
public void removeDriverDevice(DeviceDriver deviceDriver) {
deviceDriver.setDriver(null);
driverDevices.remove(deviceDriver);
}
}
...
}
and this other domain object
#Entity
#Table(name = "t_device_driver")
public class DeviceDriver implements Serializable {
/**
*
*/
private static final long serialVersionUID = 1L;
public DeviceDriver() {
}
public DeviceDriver (SeblocDevice device, Driver driver) {
this.device = device;
this.driver = driver;
}
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private long id;
#ManyToOne(fetch = FetchType.EAGER)
#JoinColumn(name = "device_id")
private SeblocDevice device;
#ManyToOne(fetch = FetchType.EAGER)
#JoinColumn(name = "driver_id")
private Driver driver;
public SeblocDevice getDevice() {
return device;
}
public void setDevice(SeblocDevice device) {
this.device = device;
}
public Driver getDriver() {
return driver;
}
public void setDriver(Driver driver) {
this.driver = driver;
}
}
and this JUnit test, where in the last test I was excepting 1 driver but I got 2 (clear all the drivers, and add 1)
#Test
public void testUpdateAuthorizedDriver() {
SeblocDevice seblocDevice = (SeblocDevice) deviceService.findById((long)1);
assertEquals (1,seblocDevice.getDriverDevices().size());
Driver authorizedDriver = (Driver) driverService.findById((long)2);
DeviceDriver dd = new DeviceDriver (seblocDevice, authorizedDriver);
DeviceDriver ddToRemove = seblocDevice.getDeviceDrivers().iterator().next();
seblocDevice.removeDriverDevice(ddToRemove);
seblocDevice.clearDriverDevices()
seblocDevice.getDriverDevices().clear();
seblocDevice.getDriverDevices().add(dd);
deviceService.save(seblocDevice);
assertEquals (1, seblocDevice.getDriverDevices().size());
assertEquals (1, Iterators.size(deviceService.findSeblocDeviceAll().iterator()));
SeblocDevice seblocDeviceRetrieved = deviceService.findSeblocDeviceAll().iterator().next();
assertEquals (1, seblocDeviceRetrieved.getDriverDevices().size());
}
I also tried to create a method in the service level
public interface DeviceDriverRepository extends CrudRepository<DeviceDriver, Long> {
}
#Transactional
public SeblocDevice cleanDrivers (SeblocDevice seblocDevice) {
deviceDriverRepository.delete(seblocDevice.getDeviceDrivers());
seblocDevice.getDeviceDrivers().clear();
seblocDevice.setDeviceDrivers(null);
return seblocDeviceRepository.save (seblocDevice);
}
and then deviceService.cleanDrivers(seblocDevice);
but the drivers appears again
crizzis is right you have to set device to null.
The best way to keep a bidirectional association consistent is to create convenience methods like:
public void addDriverDevice(DeviceDriver deviceDriver) {
deviceDriver.setDriver(deviceDriver);
driverDevices.add(deviceDriver);
}
public void removeDriverDevice(DeviceDriver deviceDriver) {
deviceDriver.setDriver(null);
driverDevices.remove(deviceDriver);
}
And if you want to clear all
public void clearDriverDevices() {
for (DeviceDriver deviceDriver : deviceDrivers) {
deviceDriver.setDriver(null);
driverDevices.remove(deviceDriver);
}
}
For your code to work as you expect, you need to add the orphanRemoval=true parameter in #OneToMany relationship in SeblocDevice.driverDevices attribute as shown below:
#OneToMany(mappedBy = "device", cascade = CascadeType.ALL, fetch = FetchType.EAGER, orphanRemoval = true)
private Set<DeviceDriver> driverDevices = new HashSet<>();
To clearly understand the JPA mapping. You need to keep in mind that in a relationship, there is always the owner side.
For example, in an #OneToMany vs. #ManyToOne relationship, #ManyToOne is the owner because it has the reference to the other entity.
Basically the Entity Manager only cares for changes on the owner side, ie if you invoke DeviceDriver.setDevice(null), the removal will be performed. But the opposite
(SeblocDevice.getDriverDevices().clear()) is not true.
For this reason, there is the orphanRemoval parameter, which is self explanatory. When this parameter is assigned, the Entity Manager will now control the elements of the collection as an owner, and a SeblocDevice.getDriverDevices().clear() will remove the DeviceDrivers in database that are not in the SeblocDevice.getDriverDevices collection, even though DeviceDriver.device is not null.
I have an issue using Ebean to save a list of object.
I have a three class. the last one included two children class.
#Entity
#Table(name="A")
public class A extends Model {
#Id
public String idA;
#OneToMany(cascade=CascadeType.ALL, mappedBy = "currentA")
private List<B> listOfB;
}
The second class B :
#Entity
public class B extends Model {
#Id
#GeneratedValue(strategy= GenerationType.IDENTITY)
public Long idB;
#ManyToOne
#JoinColumn(name = "idA")
private A currentA;
#OneToMany(cascade=CascadeType.ALL, mappedBy = "currentB")
public ArrayList<C> lstOfC;
public B(List<C> lstC) {
this.lstOfC=lstC;
}
}
And the last one :
#Entity
#Inheritance(strategy = InheritanceType.SINGLE_TABLE)
#DiscriminatorValue("X")
public class C extends Model {
#ManyToOne
#JoinColumn(name = "idB")
private B currentB;
private setcurrentB(int currentB) {
this.currentB=currentB;
}
}
The problem I am facing is that I need to create a list of C object (lstOfC) but I do not know the value of currentB when I put the element in the list.
I need to set (with a setter) this value later then save it to the database.
But when I try that, the list of C object is null from the list of B in A.
ArrayList<C> lstC=new ArrayList<C>();
c1=new C();
c2=new C();
B=new B(lstC);
for (C c: lstC) {
c.setcurrentB(1);
Ebean.save(c);
}
You example doesn't quite make sense where setcurrentB(1) ... takes 1 but expects an instance of B - I presume that is a reference bean.
It seems like you want to temporarily turn off cascade persist and you can do that on the Transaction.
Transaction tranaction = Ebean.beginTransaction();
try {
// turn off persist cascade for this transaction
transaction.setPersistCascade(false);
for (C c: listC) {
}
Ebean.commitTransaction();
} finally {
Ebean.endTransaction();
}