Assume the following model:
#Entity
public class A {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#OneToMany(mappedBy = "a", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY)
private List<B> bs;
public B getB(long id) {
for(B b : bs)
if(b.getId() == id) {
return b;
}
}
}
#Entity
public class B {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#ManyToOne
#JoinColumn(name = "a_id")
private A a;
private String someString;
}
I then try to update a property of some entity B :
#Transactional(rollbackFor = Exception.class)
public void doSomeWork() {
A a = aRepository.findById(/* some id */);
a.getB(/* */).setSomeString(/* some string */);
}
When the method returns, I expect the modified entity B to be updated (SQL UPDATE). For some reason, it doesn't happen. I suspect that the framework is only aware about additions/removals to the bs collection, but since every instance in the collection should be a managed entity, the framework should be aware of the changes.
Not sure what I'm missing here.
EDIT:
I created a repository to reproduce the issue:
https://github.com/mikomarrache/hibernate-spring-issue
If you comment lines 25-27 of the MyServiceImpl class, the save in line 22 is performed. However, if you uncomment these lines, it looks like the save in line 22 is ignored but the second save in line 27 is done, and of course it breaks the unique constraint on name. In order to test, simply run the unit test. No need to populate the database, there is an SQL script on the classpath that is executed at startup.
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.
I have two entities mapped to one another using the oneToMany annotation. One entity is bookedBus and the second is drivers The drivers entity would already have a row inserted into that would later become a foreign reference (FK) to bookedBus entity(PK). Below are the two entities, setters and getter have been skipped for brevity.
First entity
#Entity
#Table(name = "bookedBuses")
public class BookedBuses implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
#JoinColumn(name = "driver_id")
private Drivers driver;
}
Second entity
#Entity
public class Drivers implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#OneToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true, mappedBy = "driver")
private List<BookedBuses> bookedBus;
}
Now When I try to save to the booked bus entity it throws the following exception
org.springframework.dao.InvalidDataAccessApiUsageException: detached entity passed to persist: com.bus.api.entity.Drivers; nested exception is org.hibernate.PersistentObjectException: detached entity passed to persist: com.bus.api.entity.Drivers
Below is how I tried saving to the bookedBus entity
BookedBuses bookedRecord = new BookedBuses();
bookedRecord.setBookedSeats(1);
bookedRecord.setBookedBusState(BookedBusState.LOADING);
bookedRecord.setBus(busService.getBusByPlateNumber(booking.getPlateNumber()));
bookedRecord.setRoute(booking.getRoute());
infoLogger.info("GETTING DRIVER ID ======= " + booking.getDriver().getId());
Drivers drivers = new Drivers(booking.getDriver().getId());
List<BookedBuses> d_bu = new ArrayList<>();
drivers.setBooked(d_bu);
drivers.addBooked(bookedRecord);
bookedRecord.setDriver(drivers);
bookedBusService.save(bookedRecord);
My BookBusService Save Method as requested
#Autowired
private BookedBusRepository bookedBusRepo;
public boolean save(BookedBuses bookedRecord) {
try {
bookedBusRepo.save(bookedRecord);
return true;
} catch (DataIntegrityViolationException ex) {
System.out.println(ex);
AppConfig.LOGGER.error(ex);
return false;
// Log error message
}
}
1st you have some mix up in naming: you have Driver & Drivers. Like this:
private Drivers driver;
Also selecting variable names like this:
BookedBuses bookedRecord = new BookedBuses();
will cause a lot of confusion. Do not mix plural & singular between types and preferably also do not introduce names that might not be easily associated like record. Also this:
private List<BookedBuses> bookedBus;
which should rather be like:
private List<BookedBus> bookedBuses;
(and would alsoi require change to your class name BookedBuses -> BookedBus)
Anyway the actual problem seems to lie here:
Drivers drivers = new Drivers(booking.getDriver().getId());
You need to fetch existing entity by id with a help of repository instead of creating a new one with id of existing. So something like:
Drivers drivers = driverRepo.findOne(booking.getDriver().getId()); // or findById(..)
It seems that you have a constructor (that you did not show) that enables to create a driver with id. That is not managed it is considered as detached. (You also have drivers.addBooked(bookedRecord); which you did not share but maybe it is trivial)
Note also some posts suggest to changeCascadeType.ALL to CascadeType.MERGE whether that works depends on your needs. Spring data is able to do some merging on save(..) based on entity id but not necessarily in this case.
This line
Drivers drivers = new Drivers(booking.getDriver().getId());
If you already have the driver ID available with you then there's no need to pull the driver ID again from the DB.
After removing the Cascade attribute from #OneToMany & #ManyToOne your code should work.
#Entity
#Table(name = "bookedBuses")
public class BookedBuses implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
`
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "driver_id")
private Drivers driver;
}
#Entity
public class Drivers implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#OneToMany(fetch = FetchType.LAZY)
#JoinColumn(name = "driver_id")
private List<BookedBuses> bookedBus;
}
I want to get the children collection of the owner entity of a one to many relationship.
I have those two entities :
#Entity
#Table(name = "commande")
public class Commande implements Serializable {
private static final long serialVersionUID = 1L;
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
#Column(name = "name")
private String name;
#OneToMany(mappedBy = "commande")
#JoinColumn(name= "papa_id")
#JsonIgnore
private Set<Piece> pieces = new HashSet<>();
#Entity
#Table(name = "piece")
public class Piece implements Serializable {
private static final long serialVersionUID = 1L;
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
#Column(name = "ref")
private String ref;
#ManyToOne
private Commande commande;
And the resource :
#RequestMapping(value = "/papas/{id}",
method = RequestMethod.GET,
produces = MediaType.APPLICATION_JSON_VALUE)
#Timed
#Transactional
public ResponseEntity<Papa> getPapa(#PathVariable Long id) {
log.debug("REST request to get Papa : {}", id);
Papa papa = papaRepository.findOne(id);
papa.getEnfants().size();
...
}
I had to put #Transactional in order to make the .size() work (otherwise I have an exception).
It works.
But if in another method I delete one Enfant entity, then if I call getPapa again, I am getting an error durint .size() :
Unable to find com.myapp.stagiaireproject.domain.Enfant with id 3
Is it a problem of a transaction not closed?
For the first time, move all work with repository to service and mark this method as #Transactional. Set #Transactional annotation to controller method is bad practice.
One-to-many annotation is lazy by default, you can explicity set #OneToMany(mappedBy = "commande", fetch = FetchType.EAGER)
to told hibernate to read collection from database when fetching this entity.
Returning to you question: if you done this is one transaction, hibernate uses fist level cache (wich is not disabled), i.e. it caches entities, which loads by PK (id) during the transaction. And if you moves all work with repository to service class, your transaction commited before you return data from controller, and next request will read data from database, not from cache.
I have three entities, say A, B and C. A contains multiple instances of B and C. B and C have String fields as their primary key which are meant to be populated at runtime. The codes are as follows:
Class A:
#Entity
public class A
{
private int id;
private B[] bSet = null;
private C[] cSet = null;
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
public int getId()
{return id;}
#OneToMany(cascade = CascadeType.ALL, mappedBy = "a")
#OrderColumn(name = "bIndex")
public B[] getBset()
{return bSet;}
#OneToMany(cascade = CascadeType.ALL, mappedBy = "a")
#OrderColumn(name = "cIndex")
public C[] getCset()
{return cSet;}
}
Class B:
#Entity
public class B
{
private String bId;
private A a;
#Id
public String getBId()
{return bId;}
#ManyToOne(cascade = CascadeType.ALL)
public A getA(A a)
{return a;}
}
Class C:
#Entity
public class C
{
private String cId;
private A a;
#Id
public String getCId()
{return cId;}
#ManyToOne(cascade = CascadeType.ALL)
public A getA(A a)
{return a;}
}
If I try to persist a B entity in my application, Hibernate does not go for an insert, as I saw after enabling Hibernate logging. It is trying for an update, and throwing this exception.
org.hibernate.StaleStateException: Batch update returned unexpected row count from update [0]; actual row count: 0; expected: 1
This problem hasn't been answered satisfactorily anywhere. I tried here, here and here. As they say, Hibernate expects a primary id of null while saving, and if it's anything else, it goes for updating the row, and throws this exception. But I need to insert rows with these Strings as primary keys. So what can I do for that? Please help me with this.
ADDITIONAL INFO
I think I'm fighting a losing battle here. It seems that this is a bug in Hibernate itself, and until Hibernate devs solve it, we can't do this. Just look here.
I am trying to configure this #OneToMany and #ManyToOne relationship but it's simply not working, not sure why. I have done this before on other projects but somehow it's not working with my current configuration, here's the code:
public class Parent {
#OneToMany(mappedBy = "ex", fetch= FetchType.LAZY, cascade=CascadeType.ALL)
private List<Child> myChilds;
public List<Child> getMyChilds() {
return myChilds;
}
}
public class Child {
#Id
#ManyToOne(fetch=FetchType.LAZY)
private Parent ex;
#Id
private String a;
#Id
private String b;
public Parent getParent(){
return ex;
}
}
At first, I thought it could be the triple #Id annotation that was causing the malfunction, but after removing the annotations it still doesn't work. So, if anyone have any idea, I am using EclipseLink 2.0.
I just try to execute the code with some records and it returns s==0 always:
Parent p = new Parent();
Integer s = p.getMyChilds().size();
Why?
The problem most probably is in your saving because you must not be setting the parent object reference in the child you want to save, and not with your retrieval or entity mappings per se.
That could be confirmed from the database row which must be having null in the foreign key column of your child's table. e.g. to save it properly
Parent p = new Parent();
Child child = new Child();
p.setChild(child);
child.setParent(p);
save(p);
PS. It is good practice to use #JoinColumn(name = "fk_parent_id", nullable = false) with #ManyToOne annotation. This would have stopped the error while setting the value which resulted in their miss while you are trying to retrieve.
All entities need to have an #Id field and a empty constructor.
If you use custom sql scripts for initialize your database you need to add the annotation #JoinColumn on each fields who match a foreign key :
example :
class Parent {
#Id
#GeneratedValue(strategy=GenerationType.IDENTITY)
private int id;
public Parent() {}
/* Getters & Setters */
}
class Child {
#Id
#GeneratedValue(strategy=GenerationType.IDENTITY)
private int id;
/* name="<tablename>_<column>" */
#JoinColumn(name="Parent_id", referencedColumnName="id")
private int foreignParentKey;
public Child () {}
}
fetch= FetchType.LAZY
Your collection is not loaded and the transaction has ended.