I have a Spring Boot project (with JHipster) with 2 JPA Entities : User and Film.
I've created an unidirectionnal ManyToMany relationship between them.
User is the owner of the relationship.
I would like to add films into favorite list of films of user (property 'favorites' in User entity).
But when I try to add a film into favorites list, nothing is persisted into table 'user_film_favorite' (join table between the 2 entities).
The mapping seems ok because when I manually enter data in this join table, I'm able to retrieve the list of films for a user.
I've looked for a lot of similar issues here but can't find where the problem is.
Entity User :
#Entity
#Table(name = "jhi_user")
#Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE)
public class User extends AbstractAuditingEntity implements Serializable {
private static final long serialVersionUID = 1L;
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
// Other properties
#ManyToMany(cascade = CascadeType.PERSIST)
#JoinTable(
name = "user_film_favorite",
joinColumns = { #JoinColumn(name = "user_id", referencedColumnName = "id") },
inverseJoinColumns = { #JoinColumn(name = "movie_id", referencedColumnName = "id") }
)
#Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE)
#BatchSize(size = 20)
private List<Film> favorites = new ArrayList<>();
Entity Film :
#Entity
#Table(name = "film")
#Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
public class Film implements Serializable {
private static final long serialVersionUID = 1L;
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "id")
private Long id;
#NotNull
#Column(name = "title", nullable = false)
private String title;
#Column(name = "plot")
private String plot;
#Column(name = "rating")
private Float rating;
FilmService :
/**
* Add one film to current user favorites.
*
* #param id the id of the film.
* #return the film saved into user favorites.
*/
#Transactional(readOnly = true)
public Optional<FilmDTO> addToFavorites(Long id) {
log.debug("Request to get Film : {}", id);
Optional<Film> filmOpt = filmRepository.findById(id);
// Get current logged user with his favorites
Optional<User> userOpt = userService.getUserWithFavorites();
if (filmOpt.isPresent() && userOpt.isPresent()) {
User user = userOpt.get();
user.getFavorites().add(filmOpt.get());
userService.save(user);
}
return filmOpt.map(filmMapper::toDto);
}
UserService :
/**
* Save a user.
*
* #param user the entity to save.
* #return the persisted entity.
*/
public User save(User user) {
log.debug("Request to save User : {}", user);
return userRepository.save(user);
}
If anyone could help me that would be really cool ! Thanks in advance :)
You are reading the User from the database so calling save will call EntityManger.merge. Therefor you also need to add
CascadeType.MERGE
to the ManyToMany mapping.
Related
I have two entities with one-to-mane relationship:
#Entity
public class User implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "id", nullable = false)
private Long id;
#OneToMany(mappedBy = "user", cascade = CascadeType.ALL)
#JsonManagedReference
private List<RealEstate> realEstates = new ArrayList<>();
#Entity
public class RealEstate implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.SEQUENCE)
private Long id;
private String name;
#ManyToOne(cascade = CascadeType.ALL)
#JoinColumn(name = "user_id")
#JsonBackReference
private User user;
I try to save realEstate entity with this code:
realEstate.setUser(user);
realEstateService.saveRealEstate(realEstate);
And this response:
[
{
"id": 1,
"name" : "Bueno",
"user" : 1
}
]
By all I have is creating new record in user table and relation with this new ID.
What I do wrong? What I need to read about this?
You first need to save User entity record and then RealEstate entity record.
Please read this article to implement One To Many relationship. I am sure your issue will be resolved.
https://attacomsian.com/blog/spring-data-jpa-one-to-many-mapping
User.java
#Entity
#Table(name = "users")
public class User implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "user_role_id", referencedColumnName = "id")
private UserRole userRole;
}
UserRole.java
#Data
#Entity
#Table(name = "user_roles")
public class UserRole implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
}
Client.java
#Data
#Entity
#Table(name = "clients")
public class Client implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
#OneToOne(cascade = CascadeType.ALL)
#JoinColumns({ #JoinColumn(name = "user_id", referencedColumnName = "id"),
#JoinColumn(name = "user_role_id", referencedColumnName = "user_role_id") })
private User user;
}
Error
org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'entityManagerFactory' defined in class path resource [org/springframework/boot/autoconfigure/orm/jpa/HibernateJpaConfiguration.class]: Invocation of init method failed; nested exception is org.hibernate.MappingException: Unable to find column with logical name: user_role_id in users
In RDBMS, users.(id, user_role_id) is unique so clients table can refer to that.
Last time, I was using insertable = false, updatable = false on user_role_id, but when I want to add records of new client, I always need to add user_role_id manually user.setUserRoleId(userRole.getId()) after user.setUserRole(userRole) and I think that is bad practice of ORM (it should be added automatically when I set user.setUserRole(userRole))
#Column(name = "user_role_id", insertable = false, updatable = false)
private Integer userRoleId;
What should I do so the relation can be mapped in Spring JPA? and what is the best practice?
In other words, this is also mean how to reference to foreign key generated logical name column?
OK! Please try following configuration:
Below is a important code part and under this link you may find repository with working example
UserRole.java
#Data
#Entity
#Table(name = "user_roles")
public class UserRole implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "role_id")
private Integer roleId;
}
User.java
#Data
#Entity
#Table(name = "users")
public class User implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "user_id")
private Integer userId;
#ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
#JoinColumn(name = "user_role_id", referencedColumnName = "role_id")
private UserRole userRole;
}
Client.java
#Data
#Entity
#Table(name = "clients")
public class Client implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#Column(name = "client_id")
private Integer clientId;
#OneToOne(fetch = FetchType.LAZY)
#JoinColumns(
value = {
#JoinColumn(name = "client_role_id", referencedColumnName = "user_role_id"),
#JoinColumn(name = "client_user_id", referencedColumnName = "user_id"),
}
,
foreignKey = #ForeignKey(
name = "FK_user_with_role",
foreignKeyDefinition = "FOREIGN KEY (client_user_id, client_role_id)\n" +
" REFERENCES users \n" +
" (user_id, user_role_id) \n" +
" ON UPDATE CASCADE\n" +
" ON DELETE CASCADE")
)
private User user;
}
Please note that beside adding a foreignKey in the Client implementation, you MUST keep the sequence of #JoinColum annotations.. I don't know what is the reason behind, but if you flip those lines you'll still get your error as it was before :)
EDIT: I've added another answer which fits best in my opinion. I'm leaving this one as well to see the other steps I tried.
Though the solution is not elegant and not using JPA as requested. Just in case anything in here would be helpful
If I understand the main issue correctly - you want to bind Client entity with Role entity via User entity, by first setting User's Role and then transfer that "property" by using only UserId instead setting additionally RoleId while creating Client.
Basically after playing for a while with your model I think the main issue is to assign data to each other within a #Transactional methods. That seems to be caused ba Lazy fetch strategy.
My proposal for solution that binds all your Entities according expectations differs only from yours with ommiting the RoleId JoinColumn in Clients table. I have checked that when calling a service that would have #Transactional methods, you can assign a Role to the User and User to the Client with simple user.setRole(roleEntity) followed by client.setUser(userEntity).
All the data is then consistent. No need to call further like getters and setters as you mentioned in the second part of your question. Question is if for any reason you need to have RoleId as well in your Clients Table, then this soultion would have to be enhanced by additional column?
UserRole.java
#Entity
#Table(name = "user_roles")
public class UserRole implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "role_id")
private Integer roleId;
//getters and setters and toString
}
User.java
#Entity
#Table(name = "users")
public class User implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "user_id")
private Integer userId;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "user_role_id", referencedColumnName = "role_id")
private UserRole userRole;;
//getters and setters and toString;
}
Client.java
#Entity
#Table(name = "clients")
public class Client implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#Column(name = "client_id")
private Integer clientId;
#OneToOne(fetch = FetchType.LAZY)
#JoinColumns({
#JoinColumn(name = "client_user_id", referencedColumnName = "user_id"),
})
private User user;
#Column(name = "client_role_id")
private Integer roleId;
#PrePersist
#PreUpdate
private void prePersist(){
try {
roleId = getUser().getUserRole().getRoleId();
} catch (NullPointerException e){
roleId = null;
}
}
//getters and setters and toString
}
UserService.java
#Service
public class UserService {
UserRepo userRepo;
public UserService(UserRepo userRepo) {
this.userRepo = userRepo;
}
#Transactional
public void save(User user) {
userRepo.save(user);
}
#Transactional
public User getReferenceById(int i) {
return userRepo.getReferenceById(i);
}
}
ClientService.java
#Service
public class ClientService {
private ClientRepo clientRepo;
private UserService userService;
public ClientService(ClientRepo clientRepo, UserService userService) {
this.clientRepo = clientRepo;
this.userService = userService;
}
#Transactional
public Client save(Client client){
return clientRepo.save(client);
}
#Transactional
public Client getReferenceById(int i) {
return clientRepo.getReferenceById(i);
}
#Transactional
public void printClient(Client client){
client = clientRepo.getReferenceById(client.getClientId());
System.out.println(client);
}
#Transactional
public void bindUserToClient(int userId, int clientId) {
Client entity = clientRepo.findById(clientId).orElseGet(Client::new);
entity.setUser(userService.getReferenceById(userId));
}
#Transactional
public void printClient(int i) {
clientRepo.findById(i).ifPresentOrElse(this::printClient, EntityNotFoundException::new);
}
}
This configuration after running this commandLineRunner:
#Configuration
public class Config {
#Bean
#Transactional
public CommandLineRunner commandLineRunner(
#Autowired UserRoleRepo roleRepo,
#Autowired UserService userService,
#Autowired ClientService clientService
) {
return args -> {
for (int i = 0; i < 5; i++) {
roleRepo.save(new UserRole());
}
for (int i = 5; i > 0; i--) {
User user = new User();
user.setUserRole(roleRepo.getReferenceById(i));
userService.save(user);
}
Client client = new Client();
client.setUser(userService.getReferenceById(2));
client = clientService.save(client);
clientService.printClient(client);
client = new Client();
client.setClientId(1);
clientService.printClient(client);
int userId = 5;
clientService.bindUserToClient(userId, 1);
clientService.printClient(1);
};
}
}
gave me correct output in the console:
Client{id=1, user=User{id=2, userRole=UserRole{id=4}}}
Client{id=1, user=User{id=2, userRole=UserRole{id=4}}}
Client{id=1, user=User{id=5, userRole=UserRole{id=1}}}
WORKAROUND
I tried to reach the goal by use of Spring JPA but could'nt.
The workaround that keeps the referential integrity was by creating a constrains through DB like below and add #PrePersist and #PreUpdate annotated method which is updating the client's roleId as intended.
create table clients
(
client_id integer not null,
client_user_id integer,
client_role_id integer,
primary key (client_id)
);
create table user_roles
(
role_id integer generated by default as identity,
primary key (role_id)
);
create table users
(
user_id integer generated by default as identity,
user_role_id integer,
primary key (user_id),
CONSTRAINT User_Role UNIQUE (user_id, user_role_id)
);
alter table users
add constraint FK_role_id foreign key (user_role_id) references user_roles (role_id);
alter table clients
add constraint FK_user_id foreign key (client_user_id, client_role_id) references users (user_id, user_role_id) on update cascade ;
Thanks to that I could for instance update userRole in user entity, and the change was reflected in the clients table as well without any further actions
Having this entities:
User.java:
#Entity
#NoArgsConstructor
#Getter
#Setter
public class User {
#Id #GeneratedValue
private Long id;
private String username;
#OneToMany(mappedBy = "owner", cascade = CascadeType.ALL, fetch = FetchType.EAGER)
#MapKey(name = "friend_id")
private Map<User, Friendship> friends = new HashMap<>();
}
Friendship.java:
#Entity
#Data
#IdClass(Friendship.class)
public class Friendship implements Serializable {
#Id
private Long owner_id;
#Id
private Long friend_id;
private String level;
#ManyToOne(cascade = CascadeType.ALL)
#MapsId("owner_id")
private User owner;
#ManyToOne(cascade = CascadeType.ALL)
#MapsId("friend_id")
private User friend;
}
and DemoApplication.java:
#Bean
public CommandLineRunner loadData(UserRepository userRepo){
return new CommandLineRunner() {
#Override
public void run(String... args) throws Exception {
User owner = new User();
owner.setUsername("owner");
User f1 = new User();
f1.setUsername("f1");
User f2 = new User();
f2.setUsername("f2");
Friendship fs1 = new Friendship();
fs1.setOwner(owner);
fs1.setFriend(f1);
Friendship fs2 = new Friendship();
fs2.setOwner(owner);
fs2.setFriend(f2);
owner.getFriends().put(f1, fs1);
owner.getFriends().put(f2, fs2);
userRepo.saveAndFlush(owner);
}
};
}
I get error:
A different object with the same identifier value was already associated with the session : [com.example.demo.model.Friendship#Friendship(owner_id=null, friend_id=null, level=null, owner=com.example.demo.model.User#2b036135, friend=com.example.demo.model.User#a9e28af9)]
Which means both Users f1 and f2, are having null in Long id. The indeed have, when the object is created, but I thought the mapping had specified CascadeType.ALL and #GeneratedValue so the if should be created.
But I had try to set the ids myself:
...
f1.setUsername("f1");
f1.setId(1L);
User f2 = new User();
f2.setUsername("f2");
f2.setId(2L);
...
But now I got
detached entity passed to persist: com.example.demo.model.User
So I guess I should let the creation of primary keys on JPA. But as you saw from above, it does not that even with Cascading. So what now?
Try adding this under your #Id annotation
#Id
#GeneratedValue(strategy=GenerationType.IDENTITY)
I am mapping a collection with a collection.Its getting mapped successfully most of time but sometime its getting failed and i am getting null values.
ArrayList<RiskRequestDTO> riskList = new ArrayList<>();
Iterable<Risk> risks = RiskRepository.findAll();
if (risks != null) {
java.lang.reflect.Type targetListType = new TypeToken<List<riskRequestDTO>>() {
}.getType();
riskList = modelMapper.map(risks, targetListType);
}
Risk DTO
i am facing some issue to add complete code so i removed setters and getters.I am confirming that the relevant annotations are present with setters and getters
#JsonInclude(JsonInclude.Include.NON_NULL)
#JsonPropertyOrder({
"riskId", "localeTranslations", "lastModifiedAt", "lastModifiedBy"
})
public class RiskRequestDTO {
#JsonProperty("riskId")
private int riskId;
#JsonProperty("localeTranslations")
private Set<LocaleTranslation> localeTranslations;
#JsonProperty("lastModifiedAt")
private Date lastModifiedAt;
#JsonProperty("lastModifiedBy")
private UserViewDTO lastModifiedBy;
Risk Model
I am facing some issue to add complete code so i removed setters and getters of these entity
#Entity
#Table(name = "risk")
public class Risk {
/**
* risk_id , primary key.
*/
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#Column(name = “risk_id")
private int riskId;
/**
* last modified date.
*/
#Column(name = "last_modified_at")
private Date lastModifiedAt;
/**
* details of locale.
*/
#ManyToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
#JoinTable(name="risk_to_local_translation", joinColumns={ #JoinColumn(name="risk_id") }, inverseJoinColumns={ #JoinColumn(name="locale_translation_id") })
private Set<LocaleTranslation> localeTranslations;
/**
* details of the user who updated the recommendations.
*/
#ManyToOne( fetch = FetchType.EAGER, optional = false)
#JoinColumn(name = "last_modified_by", nullable = false)
private User lastModifiedBy;
I have updated question with DTO and Entity model
I have 'Course' and 'Student' entities. They have many-to-many relation. So, i have COURSE_STUDENT(contains 'student_id' and 'course_id' columns) table. I want to register students to courses with a button.(For example; a student lists courses and click Register button to register a specific course).
When i want to create new courses, i use courseRepository and courseMapper which comes from JHipster by default.
But i don't have repository and mapper files for COURSE_STUDENT. Because it is not actually a main entity. It is created for many-to-many relation.
How can i register students to courses?
Git repo:https://github.com/canberkizgi/monolithic-mucs
My course entity:
#Entity
#Table(name = "course")
#Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE)
public class Course implements Serializable {
private static final long serialVersionUID = 1L;
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#NotNull
#Column(name = "title", nullable = false)
private String title;
#Column(name = "description")
private String description;
#ManyToOne
private Instructor instructor;
#ManyToMany(fetch = FetchType.EAGER)
#Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE)
#JoinTable(name = "course_student",
joinColumns = #JoinColumn(name="courses_id", referencedColumnName="id"),
inverseJoinColumns = #JoinColumn(name="students_id", referencedColumnName="id"))
private Set<Student> students = new HashSet<>();
Student entity:
#Entity
#Table(name = "student")
#Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE)
public class Student implements Serializable {
private static final long serialVersionUID = 1L;
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#OneToOne
#JoinColumn(unique = true)
private User user;
#ManyToMany(fetch = FetchType.EAGER,mappedBy = "students")
#JsonIgnore
#Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE)
private Set<Course> courses = new HashSet<>();
For example; Createcourse function with Mapper and Repository
#PostMapping("/courses")
#Timed
public ResponseEntity<CourseDTO> createCourse(#Valid #RequestBody CourseDTO courseDTO) throws URISyntaxException {
log.debug("REST request to save Course : {}", courseDTO);
if (courseDTO.getId() != null) {
return ResponseEntity.badRequest().headers(HeaderUtil.createFailureAlert(ENTITY_NAME, "idexists", "A new course cannot already have an ID")).body(null);
}
Course course = courseMapper.toEntity(courseDTO);
course = courseRepository.save(course);
CourseDTO result = courseMapper.toDto(course);
return ResponseEntity.created(new URI("/api/courses/" + result.getId()))
.headers(HeaderUtil.createEntityCreationAlert(ENTITY_NAME, result.getId().toString()))
.body(result);
}
The relationship is owned by the course entity. Thats because on the student side the #ManyToMany annotation has a mappedBy attribute. This means, that the database will reflect the set in the course. You need to add students to that set to save the relationship. That change needs to be done within a transaction.
That being said it would probably be best to follow DDD here. I would create a registerTo method in the student class that would take the course as a parameter. I would then call this.courses.add(course) and course.getStudents().add(this) in that method.