Yet another thread like this. I fighting with this for 4 days.
annotation #Getter and #Setter are from lombok plugin
My Place class
#Entity
public class Place {
#Getter
#Setter
#OneToMany(mappedBy = "place", targetEntity = Tag.class, cascade = CascadeType.ALL)
private Set<Tag> tags;
//...
}
Tag class which should be many
#Entity
public class Tag {
#Getter
#Setter
#ManyToOne(cascade = CascadeType.ALL)
#JoinColumn(name = "fk_place_id")
private Place place;
//...
}
I'm saving it like this
Tag tagOne = new Tag("tagOne");
Tag tagTwo = new Tag("tagTwo");
Set<Tag> tagSet = new HashSet<>();
tagSet.add(tagOne);
tagSet.add(tagTwo);
Place place = new Place();
place.setTags(tagSet);
placeService.save(place);
saving looks is the single line sessionFactory.getCurrentSession().persist(entity) on every case. Saving entity with #OneToOne mapping it works like a charm.
You have a bidirectional association. You're initializing only one side of the association, and that side is the inverse side (because it has the mappedBy attribute). Hibernate only cares about the owner side. So, for Hibernate, there is no association between the tags and the place.
Note that cascade=ALL on a ManyToXxx association doesn't make sense. If 100 tags are referencing the same place, and you delete one of these tags, you don't want to also delete the place. And even if you want to, that won't work, because 99 other tags still reference it, which will cause a referential integrity error.
Related
I have two tables joined with a OneToOne relationship, one side exists in the database. When I insert the other side I want the first side's foreign key column to update so that it knows about the relationship without having to do it manually. Is this possible.
Here's my simplified example, I am using #MappedSuperclass because I have some shared fields in most of my Entities I included it here just in case it's causing an issue.
The base entity
#MappedSuperclass
#Data
public abstract class BaseEntity {
//defines some common fields I have in all my entities such Id
}
Abstract Image class
#Entity
#Inheritance(strategy = InheritanceType.SINGLE_TABLE)
#DiscriminatorColumn(name = "dtype", discriminatorType = DiscriminatorType.STRING)
#Data
public abstract class Image extends BaseEntity {
//defines some other fields
public abstract UUID getTypeId();
}
UserProfilePhoto
#Entity
#Data
public class UserProfilePhoto extends Image {
#OneToOne(cascade = CascadeType.ALL, mappedBy = "userProfilePhoto")
private Profile profile;
#Override
public UUID getTypeId() {
return user.getId();
}
}
Profile
#Entity
public class Profile extends Base {
//defines some other fields
#OneToOne(fetch = FetchType.EAGER)
#JoinColumn()
private UserProfilePhoto profilePhoto;
}
I'm looking for the following behavior:
When UserProfilePhoto is saved to image table (with Profile ID) the corresponding ImageId is updated in Profile
When Profile is Deleted the corresponding UserProfilePhoto is deleted from image table.
When UserProfilePhoto is deleted Profile remains but the foreign key column is nulled.
From researching this I think it's possible but it's a matter of getting the annotations correct. I've tried different combinations with no luck. Is what I'm looking for even possible?
No, it is not possible the way you describe it.
Any bidirectional relationship in JPA is controlled exclusively by the side indicated by mappedBy, so you need to update that side in your code, in order to have it persisted.
You can do that by invoking the other side in the setter, or by editing the other side in the first place.
I have such a case where I need to have internally many-to-one using hibernate proxies and only id externally, here using MapsId. The issue appears when I try to save something, because the target entity is not fetched, when I set the value only on the id.
Let's take an example: I have an Account table and DeviceConfig table. Inside the DeviceConfig's class definition, I add account in a many-to-one relation and accountId in relation with #MapsId.
Now when creating, I always set a value to accountId, but never the value is picked up, and the backend throws an SQL error, because the field cannot be null.
#Table(name = "djl_device_config")
#Entity
#Getter
#Setter
#ToString
#RequiredArgsConstructor
public class DeviceConfig extends CoreEntity {
...
#JsonIgnore
#ManyToOne
#MapsId("accountId")
#JoinColumn(name = "account_id")
private Account account;
#Column(name = "account_id", insertable = false, updatable = true, nullable = true)
private UUID accountId;
}
So I suppose this is a config error on my side, but I've been reading the JPA for these three days and I still don't know what's wrong or what I should do to achieve the behaviour I expect.
That for any help you'll provide.
First of all I have had two related entities mapped with bidirectional OneToMany:
#Data
#Entity
#Table(name = "tbl_parent")
public class Parent {
#Id
#Column(name = "parent_id")
private Long id;
...
#OneToMany(mappedBy = "parent")
private List<Child> children = new ArrayList<>();
}
and
#Data
#Entity
#Table(name = "tbl_child")
#ToString(exclude = "parent")
public class Child {
#Id
#Column(name = "child_id")
private Long id;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "parent_id")
private Parent parent;
}
However, I had StackOverFlow for method findAll() for Parent entity as far as infinite loop occurred during serialization.
Then I added property in application.yml:
spring:jsp:open-in-view: false
And had another problem with serialization. To solve it I added dependency in build.gradle:
implementation "com.fasterxml.jackson.datatype:jackson-datatype-hibernate5"
And configuration class:
#Configuration
public class JacksonConfiguration {
#Bean
public JavaTimeModule javaTimeModule() {
return new JavaTimeModule();
}
#Bean
public Jdk8Module jdk8TimeModule() {
return new Jdk8Module();
}
#Bean
public Hibernate5Module hibernate5Module() {
return new Hibernate5Module();
}
}
Now I have no error but list in parent entity is always null.
What I'm doing wrong? How can I fetch all related children entities without StackOverflow?
#Data annotation from Lombok can cause stackoverflows due to circular dependencies between classes if both classes have #Data and collections/toStrings mentioning each other.
See Lombok.hashCode issue with "java.lang.StackOverflowError: null"
The issue is quite simple: you cannot hope to serialize both sides of a bidirectional association. If you try to do that, you'll get a JSON representation of Parent with a children property, and each object in children will have a parent property, with each parent containing a children property, and so on, ad nauseam.
From what you describe, I'm assuming you want to opt out of serializing the parent property, in which case you should put #JsonIgnore on Child.parent.
Also, since you enabled the HibernateModule, which by default ignores lazy properties, you should either disable it, enable its FORCE_LAZY_LOADING feature, or configure Parent.children to be eagerly fetched. The third option also eliminates the need for #JsonIgnore.
(all three options have potentially far-reaching consequences, so you need to decide which one works best for your use case; also note that having to use open-jpa-in-view is a code smell, because forcing the lazy load happens outside of any transactional context)
My User class looks like this :
#Data
#Entity
public class User {
#Id
Long userID;
#ManyToMany(mappedBy = "admins")
private List<ClassRoom> classRooms = new ArrayList<>();
}
And my ClassRoom class like this :
#Data
#Entity
public class ClassRoom {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
Long classRoomID;
#ManyToMany
#JoinTable(name ="classroom_user",
joinColumns = #JoinColumn(name = "classroom_id"),
inverseJoinColumns = #JoinColumn(name = "user_id"))
private List<User> admins = new ArrayList<>();
}
And in my UserController class, I have :
#PostMapping("user/{id}/c")
User addClassRoom(#PathVariable Long id,#RequestBody ClassRoom newClassRoom)
{
logger.debug(repository.findById(id));
return repository.findById(id)
.map(user -> {
user.getClassRooms().add(newClassRoom);
user.setClassRooms(user.getClassRooms());
return repository.save(user);
})
.orElseGet(() -> {
return null;
});
}
And I POST and empty JSON ({}) and I see no change in my users. The Classroom or an empty Classroom doesn't get added in the User.
What is the problem here? How can I resolve this ?
user.getClassRooms().add(newClassRoom); is suffice, user.setClassRooms(user.getClassRooms()); not required.
You will have to perform cascade save operation.List all cascade types explicitly and don't use mappedBy, instead use joincolumns annotation.
Can you paste the logs, please? Is Hibernate doing any insert into your table? Has the database schema been created in the DB correctly? One thing I recommend you to do is to add a custom table name on the top of your User class, using annotations like so: #Table(name = "users"). In most SQL dialects user is a reserved keyword, hence it is recommended to always annotate User class a bit differently, so that Hibernate won't have any problems to create a table for that entity.
IMO you must find classRoom by its id from repository, if it's new, you must create a new entity and save it first. Then assign it to user and save it.
The object you receive from the post method was not created by the entity manager.
After using user.getClassRooms().add(newClassRoom);
We must use userRepository.save(user);
I am planning to store data from multiple tables which has one to many JPA relationship. I am creating my Repository interface which extends from JPARepository. My question is If I want to save a data on Many sides of relationship (in the below scenario it's Tour) then shall I do with TourRepository or PersonRespository?
On a similar note Is it ideal to create individual repository classes for every JPA entities where data need to be stored? or any better way with limited repository classes the data store to database can be achieved?
#Entity
#Table(name="Person")
public class Person implements Serializable{
...
...
#OneToMany(mappedBy = "person")
private List<Tour> tours;
...
#Entity
#Table(name = "Tour")
public class Tour implements Serializable{
...
...
#ManyToOne
#JoinColumn(name = "PERSON_ID")
private Person person;
...
You have two independent entities. Person can exist without Tour and Tour can exist without Person. So you should have two repositories - for Person and Tour to store their data independently:
Tour tour1 = new Tour("tour1");
tourRepo.save(tour1);
Person person1 = new Person("person1");
person1.addTour(tour1);
personRepo.save(person1);
You chose the bidirectional one-to-many association so you have to use a 'helper' method like addTour to link both entities:
public Person addTour(Tour tour) {
tour.setPerson(this);
this.tours.add(tour);
return this;
}
Additional info: Best Practices for Many-To-One and One-To-Many Association Mappings
Add cascade to tours:
#OneToMany(mappedBy = "person", cascade = {CascadeType.PERSIST, CascadeType.MERGE})
private List<Tour> tours;
When you save person object, his tours will be saved automatically.
By the way, in Person class, you should have an addTour(...) utilities method like this:
// Person.java
public void addTour(Tour tour){
this.tours.add(tour);
tour.setPerson(this);
}
I would suggest you to use CascadeType.ALL on #OneToMany mapping in Person entity:
#OneToMany(mappedBy = "person",cascade = {CascadeType.ALL})
private List<Tour> tours;
And then create repository for person to save person object with the list of tours .
CascadeType.ALL means persistence will propagate all EntityManager operations like PERSIST, REMOVE, REFRESH, MERGE, DETACH to the relating entities.