I have to test some code I have not myself written. It is an integration test: the application is running continuously on a server and my tests run against it.
The tests are Selenium tests, they launch a browser, execute some JavaScript inside it to simulate user actions and checks if the database is correctly updated. I have to restore the database to its initial state after each.
To do this, I use Spring annotations and Hibernate via DAO's I have not myself written.
The problem is that there are circular foreign keys. An object of class A has a OneToMany relationship with objects of type B, and there is also a ManyToOne association with the same class. I try to delete an object of type A and all its associated B's in the same transaction, but it doesn't work because Hibernate tries to set "defaultB" to null before deleting the object of type A. It is completely unnecessary to nullify it, although it makes sense to do it once the referred object of type B is deleted.
I (naively) thought that because the 2 operations were executed in the same transaction, deleting the object "a" of type A referring to (and referenced by) the object "b" of class B and deleting b at the same time would be no problem. However, I was plain wrong. It there is way to do this without changing the DB model (which I haven't written)?
Update 1: I don't understand why, when I execute mySession.delete(B), Hibernate tries to nullify a key it knows as non-nullable...any thoughts about this?
Update 2: there is a one-to-many relationship from class C to class B. Hibernate also tries to nullify the "c_id" field in the table corresponding to B, when I delete the C object that has this c_id. And that even though I delete the object of class B before its "parent". I know Hibernate reorders the queries and adds some stuff of its own, but I don't get the point of reordering queries that are already in the correct order to make them fail.
Here are (relevant parts of) the classes:
#Entity
#Table(name = "A")
public class A {
private Set<B> bs;
private B defaultB;
#OneToMany(mappedBy = "a", fetch = LAZY)
public Set<B> getBs() {
return bs;
}
public void setBs(Set<B> bs) {
this.bs = bs;
}
#ManyToOne(fetch = LAZY, optional = false)
#JoinColumn(name = "default_b_id", nullable = false)
public B getDefaultB(){
return defaultB;
}
public void setDefaultB(B defaultB) {
this.defaultB = defaultB;
}
}
#Entity
#Table(name = "B")
public class B {
private a;
#ManyToOne(fetch = LAZY, optional = false)
#JoinColumn(name = "A_id", nullable = false)
public A getA() {
return a;
}
public void setA(A a) {
this.a = a;
}
}
I try to delete an object of type A and all its associated B's in the same transaction
You should cascade the REMOVE operation for this if you don't want to have to remove all Bs manually. I would try the following (using cascade on both associations):
#Entity
#Table(name = "A")
public class A {
private Set<B> bs;
private B defaultB;
#OneToMany(mappedBy = "a", fetch = LAZY, cascade=CascadeType.REMOVE)
public Set<B> getBs() {
return bs;
}
public void setBs(Set<B> bs) {
this.bs = bs;
}
#ManyToOne(fetch = LAZY, optional = false, cascade=CascadeType.REMOVE)
#JoinColumn(name = "default_b_id", nullable = false)
public Strategy getDefaultB(){
return defaultB;
}
public void setDefaultB(B defaultB) {
this.defaultB = defaultB;
}
}
I cannot change these annotations. BTW, I do remove all associated B's manually, it's just that the queries Hibernate issues don't do what I want.
Ok... But then my guess is that you're not updating correctly both sides of the bidirectional association before remove the entities. This is typically done in defensive programming methods like this (in A):
public removeFromBs(B b) {
b.setA(null);
this.getBs().remove(b);
}
I assume that you want to delete a, but Hibernate does not allow it because b still refer to it?
Since you meta-model do not specify cascade delete, you need to "break" the link of b to a before deleting a. So do b.setA(null) before deleting a.
Related
I want to build a simple HATEOAS API with Spring Boot and Spring Data REST.
Thus, I have created 2 different entities A and B. There is a Many-To-Many relation between both. Because the relation needs 2 more properties, I added a new relation R with these properties.
For the API, only A and B should be visible, R is only internal and has no repository. I'd like to represent A with a link to a collection of B.
It should be possible to create A, create B, link A to B or B to A without providing and returning the internal properties of R.
I guess it's a design problem?
Possible solutions:
I tried to create a custom method findByR_Bid() but I cant provide the own id to this method, so "/a/b" is not possible, it's always "/a/search/...".
Next, I've added a custom link with ResponseEntities.linkToCollection(B.class), but this will represent a link to "/b" without a reference to A's own id.
Next, I have implemented a method in A to return List from R. But this list is included internally as property instead of creating a Link.
A bit code:
#Entity
public class A {
#Id
private Long id;
#OneToMany(mappedBy = "a")
private List<R> r;
// getters
}
#Entity
public class B {
#Id
private Long id;
#OneToMany(mappedBy = "b")
private List<R> r;
// getters
}
#Entity
public class R {
#Id
private Long id;
#ManyToOne(optional = false)
private A a;
#ManyToOne(optional = false)
private B b;
// internal properties
#JsonIgnore
private String foo;
#JsonIgnore
private String bar;
// getters
}
For the JSON response I want to have something like this:
GET /a/1
"_links" : {
"self": {
"href": "http://localhost:8080/a/1"
},
"a": {
"href": "http://localhost:8080/a/1"
},
"bs": {
"href": "http://localhost:8080/a/1/b{?page,size,sort}",
"templated": true
}
}
It's tricky, but not impossible!
You can add B to A with a many-to-many relationship (and vica versa) using the same table:
#Entity
public class A {
#Id
private Long id;
#OneToMany(mappedBy = "a")
private List<R> r;
#ManyToMany
#JoinTable(name = "table_r", inverseJoinColumns = { #JoinColumn(name = "b_id") })
private list<B> b;
// getters
}
This way you force hibernate to use the same table for the man-to-many relationship which supports the R entity. Spring Data Rest will also map this many-to-many relationship to the endpoints, so /a/1/bs will work.
However accessing the extra properties of the relationship will be still tricky. (you need to use the /search endpoints.)
An other solution if you create a composite key for R (from A+B) instead of standard id. (Which is actually already exists, because the support table for a many-to-many relationship has a composite key based on a_id+b_id)
If you create a converter for the composite key then you can access the relationship properties like:
/R/23-34 (Relationship between a-23 and b-34)
It's not too nice but works.
I have an Entity including a collection of Embeddable objects as follows:
#Entity
#EntityListeners(AuditingEntityListener.class)
#Table(name = "as")
public class A {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#ElementCollection
private Set<B> bs;
public B getB(String name) {
for(B b : bs)
if(b.getName().equals(name))
return b;
return null;
}
public void addB(B b) {
if(!bs.add(b))
throw new IllegalArgumentException("Duplicate ......");
}
....
}
#Embeddable
public class B {
#Column(nullable = false)
private String name;
#Temporal(TemporalType.TIMESTAMP)
#Column(nullable = false)
private Date creationTimestamp;
}
I'm using Spring Data to load and save my entity as follows:
Optional<A> a = aRepository.findById(aId);
B b = a.getB(...);
if(b == null) {
b = new B(...);
a.addB(b);
}
aRepository.save(a);
The code above is in a method annotatted with #Transactional.
When the method returns, I can see 3 duplicate embeddable objects in my database instead of one.
Any idea?
EDIT:
After a long debugging, I can confirm that Hibernate only inserts only one row for the single instance I add. However, when I return the created object from my REST controller, at some point the Jackson object mapper is involved to serialize my object before sending it back to the client, and here happens the two remaining INSERTs... I never saw that before... any help would be appreciated
More information:
The last 2 INSERTs are done when the SessionRepository commits the sessions changes - I precise that I use Spring Session. If that can help...
Issue happens here bs.add(b). Set adds an object would check if the object exists in the set. But in your case object b would always not the same as existing object because of creationTimestamp. creationTimestamp would be different from existing object. Because you new object, the time would be current datatime. So bs.add(b) would always be true.
Try to remove creationTimestamp and rerun your code to verify this.
You need to override equals() and hashCode() of B
How can I pass the test? (was working before migrate my code to use repositories). The bs are stored in the database after save, but the object are not updated. What I have to do to achieve it?
Given these classes:
#Entity
public class A {
#Id
private String id;
#OneToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL)
#JoinColumn(name = "aId")
private Set<B> bs= new HashSet<B>();
...
}
#Entity
public class B {
#Id
#GeneratedValue
private int id;
private String aId;
private String foo;
...
}
And Repository:
#RepositoryDefinition(domainClass = A.class, idClass = String.class)
public interface ARepository {
...
void save(A a);
...
}
This test fail:
// "a" saved and flushed
B b = new B();
b.setAId(a.getId());
a.getBs().add(b);
ARepository.save(a);
assertTrue(b.getId() > 0);
repository.save() does persist (if the provided argument is transient) or merge (otherwise).
Since a is not transient, merge is performed, meaning that there is no persist operation that could be cascaded to bs.
You either have to save b explicitly or add b to a new a before the a is saved, so that persist is cascaded properly.
Probably, the reason is that B object is not in persisted state yet. As soon as it will be saved - you shouldn't get errors.
Should look like this:
// "a" saved and flushed
B b = new B();
BRepository.save(b)
b.setAId(a.getId());
a.getBs().add(b);
ARepository.save(a);
assertTrue(b.getId() > 0);
Also could you please provide stacktrace? Would be really helpful.
I'm having two tables -
Foo { foo_id, name }
Foo_properties { fp_id, foo_id, phoneNumber}
Now I want to map this in my object model using hibernate..
I need a foo_id in Foo_properties because i want to maintain referential integrity and want to add ON DELETE CASCADE constraint.
so I mapped the relation in the following way -
#Entity
public class Foo{
#Id
private long foo_id;
private String name;
#OneToOne(mappedBy = "foo")
private FooProperties fooProperties;
}
#Entity
public class FooProperties{
#Id
private long fp_id;
private String phoneNumber;
#OneToOne
#JoinColumn(name = "foo_id", nullable = false)
private Foo foo;
}
Now since the owning side is FooProperties class, I'm facing following issues -
If I set the new instance of FooProperties to Foo the existing FooProperties still remains in DB and hibernate doesn't delete that instance, e.g.
Foo foo = entityManager.find(Foo.class, fooId);
foo.setFooProperties(new FooProperties("xxx-xxx-xxx"));
entityManager.merge(foo);
This results into the new row in FooProperties table along with the existing one. Now I don't understand how I can change my mapping to so I can have above code (or variant of it) working for all scenarios, that means I need Foo as a owning side and foo_id in FooProperties. Is there any way to define the mapping like this?
NOTE: I already asked question based on this but I think I wasn't clear in previous question so asked this another one.
You were already told to use orphanRemoval = true or CascadeType.DELETE_ORPHAN. However, due to casuistics in interpretation of JPA Specification it wouldn't work as expected for one-to-one relationships (HHH-5559).
You can achieve a proper behaviour of orphanRemoval with the following trick:
#Entity
public class Foo{
#OneToMany(mappedBy = "foo", orphanRemoval = true)
private List<FooProperties> fooProperties;
public FooProperties getFooProperties() {
if (fooProperties == null || fooProperties.isEmpty()) return null;
else return fooProperties.get(0);
}
public void setFooProperties(FooProperties newFooProperties) {
if (fooProperties == null) fooProperties = new ArrayList<FooProperties>();
else fooProperties.clear();
if (newFooProperties != null)
fooProperties.add(newFooProperties);
}
...
}
#Entity
public class FooProperties{
#ManyToOne
#JoinColumn(name = "foo_id", nullable = false)
private Foo foo;
...
}
Or even this, if you don't need FooPropeties.foo:
#Entity
public class Foo{
#OneToMany(orphanRemoval = true)
#JoinColumn(name = "foo_id", nullable = false)
private List<FooProperties> fooProperties;
// getter/setter as above
...
}
Bar is the owner of the association (as indicated by the mappedBy on the inverse side) and thus the cascade has to be set there.
Edit:
To invert that, this might help.
There are 2 options for you to choose from, since you don't want to change your mapping :
Do it via your service layer logic. I think you have a similar question already.
Use the Hibernate annotation #Cascade(org.hibernate.annotations.CascadeType.DELETE_ORPHAN) on the Foo side of the relationship. However this is explicitly Hibenate and JPA 2 doesn't include support for the same.
I think instead of calling merge on the entity, if you directly call update on session object then hibernate will first delete the existing row and then it will add the new one. I implemented the same, but, in my case I used xml for mapping the entity. I hope this will help you.
I'm a hibernate newbie and I'm not entirely sure how to get the cascade behavior I'm looking for.
Let's say I have two classes A and B with bi-directional many-to-many mappings to each other. A is the owner side and B is the inverse side (I hope I have the terminology correct).
public class A
{
private Set<B> bSet = new HashSet<B>();
#ManyToMany(targetEntity=B.class, cascade=CascadeType.?)
public Set<B> getBSet()
{
return bSet;
}
}
public class B
{
private Set<A> aSet = new HashSet<A>();
#ManyToMany(mappedBy="bSet", targetEntity=A.class, cascade=CascadeType.?)
public Set<B> getBSet()
{
return bSet;
}
}
I want to select the correct cascade type so that when I delete an A object, any B object containing it is updated and vice-versa.
Which cascade type(s) do I need here?
CascadeType.ALL so every operation affects the other class. See link text