I created new entity and while trying to make relation beetwen some other entity, I got an error because it can't find this class.
#Entity(name = "users")
#Builder
#Data
#AllArgsConstructor
#NoArgsConstructor
public class User {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#Column(unique = true)
private String username;
private String password;
#ManyToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL)
#JoinTable(
name = "user_roles",
joinColumns = #JoinColumn(name = "user_id"),
inverseJoinColumns = #JoinColumn(name = "role_id")
)
private Collection<Role> roles = new ArrayList<>();
#OneToMany(mappedBy = "author")
List<Post> posts = new ArrayList<>();
}
I'm talking about that OneToMany List posts, I can't import Post class
#Data
#Entity
#Builder
#AllArgsConstructor
#NoArgsConstructor
public class Post {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private int Long;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "users_id")
private User author;
}
I have no idea why it's happening and how to solve it.
Just to let other knows if someone gonna have the same problem. Changing name of the entity helped and then changing it back.
Related
I have two jpa entities User + profile with many to many relationship like below :
User Entity
#Data
#Entity
#AllArgsConstructor
#NoArgsConstructor
#Table(name = "t_user")
public class UserEntity {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#Column(name = "user_id")
private Long Id;
#Column(nullable = false)
private String username;
#Column(nullable = false, length = 14)
private String siret;
#Cascade({
org.hibernate.annotations.CascadeType.SAVE_UPDATE,
org.hibernate.annotations.CascadeType.MERGE,
org.hibernate.annotations.CascadeType.PERSIST
})
#ManyToMany(fetch = FetchType.LAZY,cascade = CascadeType.MERGE)
#JoinTable(name = "t_user_profile", joinColumns = {#JoinColumn(name = "user_id")},
inverseJoinColumns = {#JoinColumn(name = "profile_id")})
private Set<ProfileEntity> profiles = new HashSet<>();
#OneToOne(fetch = FetchType.LAZY,cascade = CascadeType.ALL)
#JoinColumn(name = "ressource_id")
private RessourceEntity ressource;
}
and Profile entity :
#Data
#Entity
#AllArgsConstructor
#NoArgsConstructor
#Table(name = "t_profile")
public class ProfileEntity {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#Column(name = "profile_id")
private Long Id;
#Column(nullable = false)
#Enumerated(EnumType.STRING)
private Profiles profile;
#ManyToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL, mappedBy = "profiles")
private Set<UserEntity> user = new HashSet<>();
}
The problem is when I send an http request contaning the same profiles with different users, I obtain new duplicated profiles records with new IDs in the Profile table in the database like below :
but the correct way is to add the informations ONLY in the joined table t_user_profile because profile already exists in profile table .
I tried cascade.All , persist and merge .... but the same result .
And if I remove cascade and i search for profile Id using findById and get the Id and insert... I got a problem when the profile is new and don't exist in DB and the problem is because I removed cascade ... So I want if the profile already exists , I want to add the relationship in the joined table ONLY.
I'm new at Spring Boot's JPA concept so need your help in deciding how to import the ID of another entity and ArrayList of Ids of another entity. I want to create a board, providing an account's Id and ArrayList of Ids of accounts.
Following are my Account and Board entities:
#Entity(name = "Account")
#Table(name = "account", uniqueConstraints = {#UniqueConstraint(name = "account_email_unique", columnNames = "email")})
#Getter
#Setter
#NoArgsConstructor
#AllArgsConstructor
public class Account {
#Id
#GeneratedValue(strategy = GenerationType.SEQUENCE)
#Column(name = "account_id")
private Integer accountId;
#JsonIgnore
#OneToMany(targetEntity = Board.class, mappedBy = "boardOwnerId")
private Set<Board> boardSet = new HashSet<>();
#JsonIgnore
#ManyToMany(mappedBy = "boardMembers")
private Set<Board> boards = new HashSet<>();
#Entity(name = "Board")
#Table(name = "board", uniqueConstraints = {#UniqueConstraint(name = "board_name_unique", columnNames = "name")})
#Getter
#Setter
#NoArgsConstructor
#AllArgsConstructor
public class Board {
#Id
#GeneratedValue(strategy = GenerationType.SEQUENCE)
#Column(name = "board_id")
private Integer boardId;
#ManyToOne(targetEntity = Account.class, cascade = CascadeType.ALL)
#JoinColumn(name = "account_id", referencedColumnName = "account_id")
private Account boardOwnerId;
#ManyToMany
#JoinTable(name = "board_member", joinColumns = #JoinColumn(name = "board_id"), inverseJoinColumns =
#JoinColumn(name = "account_id"))
private Set<Account> boardMembers = new HashSet<>();
#Repository
public interface BoardRepository extends JpaRepository<Board, Integer> {
}
#RestController
#RequestMapping("/boards")
public class BoardController {
private final BoardService boardService;
#Autowired
public BoardController(BoardService boardService) {
this.boardService = boardService;
}
#PostMapping("/create-board")
ResponseEntity<BoardDtoResponse> createBoard(#Valid #RequestBody BoardDto boardDto) {
return new ResponseEntity<>(boardService.createBoard(boardDto), HttpStatus.CREATED);
}
}
#Service
public class BoardServiceImpl implements BoardService {
private final BoardRepository boardRepository;
private final ModelMapper modelMapper;
#Autowired
public BoardServiceImpl(BoardRepository boardRepository) {
this.boardRepository = boardRepository;
modelMapper = new ModelMapper();
}
#Override
public BoardDtoResponse createBoard(BoardDto boardDto) {
Board boardToSave = modelMapper.map(boardDto, Board.class);
Board newBoard = boardRepository.save(boardToSave);
return modelMapper.map(newBoard, BoardDtoResponse.class);
}
}
I can successfully create an account, but when I want to create a board and pass boardOwnerId and membersIds, it creates a board, but boardOwnerId and membersIds are set to null.
Here is the request via Postman:
Thanks in advance for your time!
As far as I have seen, you should change the mapping between the two entities for both mappings. Let me explain:
For the mapping of the board owner (#OneToMany) try to maintain only that one annotation and remove the property with #ManyToOne from Board entity. In addition, change the properties values of the #OneToMany annotation and add a #JoinColumn with next values:
#Entity(name = "Account")
#Table(name = "account", uniqueConstraints = {#UniqueConstraint(name = "account_email_unique", columnNames = "email")})
#Getter
#Setter
#NoArgsConstructor
#AllArgsConstructor
public class Account {
#Id
#GeneratedValue(strategy = GenerationType.SEQUENCE)
#Column(name = "account_id")
private Integer accountId;
#JsonIgnore
#OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY, orphanRemoval = true)
#JoinColumn(name = "boardOwnerId")
private Set<Board> boardSet = new HashSet<>();
...
#Entity(name = "Board")
#Table(name = "board", uniqueConstraints = {#UniqueConstraint(name = "board_name_unique", columnNames = "name")})
#Getter
#Setter
#NoArgsConstructor
#AllArgsConstructor
public class Board {
#Id
#GeneratedValue(strategy = GenerationType.SEQUENCE)
#Column(name = "board_id")
private Integer boardId;
...
This is known as a One To Many unidirectional mapping (https://www.bezkoder.com/jpa-one-to-many-unidirectional/).
On the other hand you could try to maintain only the #ManyToOne annotation on Board entity, but remove the property with #OneToMany annotation from Account entity with next properties values:
#Entity(name = "Account")
#Table(name = "account", uniqueConstraints = {#UniqueConstraint(name = "account_email_unique", columnNames = "email")})
#Getter
#Setter
#NoArgsConstructor
#AllArgsConstructor
public class Account {
#Id
#GeneratedValue(strategy = GenerationType.SEQUENCE)
#Column(name = "account_id")
private Integer accountId;
...
#Entity(name = "Board")
#Table(name = "board", uniqueConstraints = {#UniqueConstraint(name = "board_name_unique", columnNames = "name")})
#Getter
#Setter
#NoArgsConstructor
#AllArgsConstructor
public class Board {
#Id
#GeneratedValue(strategy = GenerationType.SEQUENCE)
#Column(name = "board_id")
private Integer boardId;
#JsonIgnore
#ManyToOne(fetch = FetchType.LAZY, optional = false)
#JoinColumn(name = "account_id", nullable = false)
#OnDelete(action = OnDeleteAction.CASCADE)
private Account boardOwnerId;
...
This is known as the default One To Many mapping (https://www.bezkoder.com/jpa-one-to-many/).
In any case, you see you only have to implement one of the two types of annotations for a One To Many mapping.
And last, for the #ManyToMany mappings, try the next implementation (adding fetch and cascade properties values):
#Entity(name = "Account")
#Table(name = "account", uniqueConstraints = {#UniqueConstraint(name = "account_email_unique", columnNames = "email")})
#Getter
#Setter
#NoArgsConstructor
#AllArgsConstructor
public class Account {
...
#ManyToMany(fetch = FetchType.LAZY,
cascade = {
CascadeType.PERSIST,
CascadeType.MERGE
},
mappedBy = "boardMembers")
#JsonIgnore
private Set<Board> boards = new HashSet<>();
#Entity(name = "Board")
#Table(name = "board", uniqueConstraints = {#UniqueConstraint(name = "board_name_unique", columnNames = "name")})
#Getter
#Setter
#NoArgsConstructor
#AllArgsConstructor
public class Board {
...
#ManyToMany(fetch = FetchType.LAZY,
cascade = {
CascadeType.PERSIST,
CascadeType.MERGE
})
#JoinTable(name = "board_member", joinColumns = #JoinColumn(name = "board_id"), inverseJoinColumns =
#JoinColumn(name = "account_id"))
private Set<Account> boardMembers = new HashSet<>();
You can find this implementation design here: https://www.bezkoder.com/jpa-many-to-many/
The problem was that the entity was not mapping properly with dto. The solution is explicit mapping plus the answer of Gescof.
Here I found information about explicit mapping: ModelMapper mapping the wrong id
Changed code in the service class:
#Service
public class BoardServiceImpl implements BoardService {
private final BoardRepository boardRepository;
private final ModelMapper modelMapper;
#Autowired
public BoardServiceImpl(BoardRepository boardRepository) {
this.boardRepository = boardRepository;
modelMapper = new ModelMapper();
modelMapper.getConfiguration().setMatchingStrategy(MatchingStrategies.LOOSE);
}
#Override
public BoardDtoResponse createBoard(BoardDto boardDto) {
Board boardToSave = modelMapper.map(boardDto, Board.class);
Board newBoard = boardRepository.save(boardToSave);
return modelMapper.map(newBoard, BoardDtoResponse.class);
}
}
Changed code in the entity classes:
#Entity(name = "Account")
#Table(name = "account", uniqueConstraints = {#UniqueConstraint(name = "account_email_unique", columnNames = "email")})
#Getter
#Setter
#NoArgsConstructor
#AllArgsConstructor
public class Account {
#Id
#GeneratedValue(strategy = GenerationType.SEQUENCE)
#Column(name = "account_id")
private Integer accountId;
#JsonIgnore
#ManyToMany(fetch = FetchType.LAZY,
cascade = {
CascadeType.PERSIST,
CascadeType.MERGE
},
mappedBy = "boardMembers")
private Set<Board> boards = new HashSet<>();
#Entity(name = "Board")
#Table(name = "board", uniqueConstraints = {#UniqueConstraint(name = "board_name_unique", columnNames = "name")})
#Getter
#Setter
#NoArgsConstructor
#AllArgsConstructor
public class Board {
#Id
#GeneratedValue(strategy = GenerationType.SEQUENCE)
#Column(name = "board_id")
private Integer boardId;
#ManyToOne(fetch = FetchType.LAZY, optional = false)
#JoinColumn(name = "account_id", nullable = false)
#OnDelete(action = OnDeleteAction.CASCADE)
private Account boardOwnerId;
#ManyToMany(fetch = FetchType.LAZY,
cascade = {
CascadeType.PERSIST,
CascadeType.MERGE
})
#JoinTable(name = "board_member", joinColumns = #JoinColumn(name = "board_id"), inverseJoinColumns =
#JoinColumn(name = "account_id"))
private Set<Account> boardMembers = new HashSet<>();
I've created an API that has actor, movie and category entities. Actor and movie are connected by many-to-many relationship that maps to a join table called movie_actor and category is connected with movie by one-to-many relationship.
I'm trying to write a native query that returns an integer that would represent the amount of movies from a specific category where specific actor has played so for example query would return 2 if actor played in 2 different sci-fi movies. I have no problem doing that from the database level where I can see the join table movie_actor but that table remains unaccessible in my api because it's not a separate entity. How can I create it that it automatically maps actor and movie ids as the movie_actor table ?
Here is an example code that works for me in the H2 Database:
SELECT COUNT(*) FROM MOVIE M JOIN MOVIE_ACTOR MA on M.MOVIE_ID = MA.MOVIE_ID WHERE ACTOR_ID = 1 AND CATEGORY_ID = 1
Here are my entities:
Actor:
#Entity
#Data
#Table(name = "actor")
#AllArgsConstructor
#NoArgsConstructor
public class Actor {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "actor_id")
private long actorId;
#Column(name = "name")
private String name;
#Column(name = "surname")
private String surname;
#Nullable
#ManyToMany(mappedBy = "actors", fetch = FetchType.EAGER)
#JsonBackReference
private List<Movie> movies = new ArrayList<>();
public Actor(String name, String surname){
this.name = name;
this.surname = surname;
}
}
Movie:
#Entity
#Data
#Table(name = "movie")
#AllArgsConstructor
#NoArgsConstructor
public class Movie {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "movie_id")
private long movieId;
#Column(name = "title")
private String title;
#ManyToMany
#JoinTable(
name = "movie_actor",
joinColumns = #JoinColumn(name = "movie_id"),
inverseJoinColumns = {#JoinColumn(name = "actor_id")}
)
#JsonManagedReference
private List<Actor> actors = new ArrayList<>();
#ManyToOne
#JoinColumn(name = "CATEGORY_ID")
#JsonManagedReference
private Category category;
}
So you have to make it accessible in your API. One option would be to map the intersection table movie_actor to the entity MovieActor and split ManyToMany relationship between Actor and Movie to OneToMany relationship with MovieActor, like that:
#Entity
#Data
#Table(name = "movie_actor")
#AllArgsConstructor
#NoArgsConstructor
public class MovieActor {
#EmbeddedId
private MovieActorId productOrderId = new MovieActorId();
#ManyToOne(cascade = CascadeType.ALL, optional = false)
#MapsId("movieId")
#JoinColumn(name = "product_id", nullable = false)
private Movie movie;
#ManyToOne(cascade = CascadeType.ALL, optional = false)
#MapsId("actorId")
#JoinColumn(name = "order_id", nullable = false)
private Actor actor;
public void addMovieActor(Movie aMovie, Actor aActor) {
movie = aMovie;
actor = aActor;
aMovie.getMovieActors().add(this);
aActor.getMovieActors().add(this);
}
}
#Embeddable
#Getter
#Setter
public class MovieActorId implements Serializable {
#Column(name = "movie_id")
private Long movieId;
#Column(name = "actor_id")
private Long actorId;
}
#Entity
#Data
#Table(name = "actor")
#AllArgsConstructor
#NoArgsConstructor
public class Actor {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "actor_id")
private long actorId;
#OneToMany(mappedBy = "actor")
private List<MovieActor> movieActors = new ArrayList<>();
}
#Entity
#Data
#Table(name = "movie")
#AllArgsConstructor
#NoArgsConstructor
public class Movie {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "movie_id")
private long movieId;
#OneToMany(mappedBy = "movie")
private List<MovieActor> movieActors = new ArrayList<>();
}
Now you can access the intersection table MovieActor inside the query. You can even add more columns to this table if you want.
I got a lot of answers and advice, tried changing my code according to these tips.
But, unfortunately, these tips helped only partially.
Now, when creating a new project and creating a new user, I can add the desired user to the set of projects, and the required project will be added to the set of users.
But the relationship between the desired project and the desired user will not appear in the project_user table.
Please help find the answer.
Entity Project
#Data
#Entity
#AllArgsConstructor
#NoArgsConstructor
#Table(name = "project")
public class Project {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#Column
private String name;
#Column
private String description;
#ManyToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
#JoinTable(name = "project_users",
joinColumns = #JoinColumn(name = "project_id"),
inverseJoinColumns = #JoinColumn(name = "users_id"))
private Set<User> projectUserSet = new HashSet<>();
#ManyToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
#JoinTable(name = "project_task",
joinColumns = #JoinColumn(name = "project_id"),
inverseJoinColumns = #JoinColumn(name = "task_id"))
private Set<Task> projectTaskSet = new HashSet<>();
public void addUserToProject(User user){
this.projectUserSet.add(user);
user.getUserProjectsSet().add(this);
}
public void addTasksToProject(Task task){
this.projectTaskSet.add(task);
task.getTasksProjectSet().add(this);
}
//constructors, hashCode, equals, toString
}
Entity User
#Data
#Entity
#AllArgsConstructor
#NoArgsConstructor
#Table(name = "users")
public class User {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#Column
private String firstName;
#Column
private String lastName;
#ManyToMany(mappedBy = "projectUserSet", cascade = CascadeType.ALL)
private Set<Project> userProjectsSet = new HashSet<>();
#ManyToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
#JoinTable(name = "users_task",
joinColumns = {#JoinColumn(name = "users_id")},
inverseJoinColumns = {#JoinColumn(name = "task_id")})
private Set<Task> userTasksSet = new HashSet<>();
public void addTaskToUser(Task task) {
this.userTasksSet.add(task);
task.getTasksUserSet().add(this);
}
//constructors, hashCode, equals, toString
}
project and user initialization
Project project1 = new Project("Project1", "Project1");
User user1 = new User("User1", "User1");
project1.addUserToProject(user1);
With code shown below, table project_user is populated, verified using H2 console. In order to avoid stack overflow, I had to modify method Project#addTaskToUser as shown below.
Please note that only code relevant to the question, is included.
Normally, issue should be described by some tests. In this case, I added a CommandLineRunner that runs at startup.
CascadeType.ALL is not recommended for many-to-many relations, hence I changed this in code shown below.
Tested using H2 in-memory db.
Project class
#Data
#Entity
public class Project {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String description;
#ManyToMany(cascade = {CascadeType.PERSIST, CascadeType.MERGE})
#JoinTable(name = "project_users",
joinColumns = #JoinColumn(name = "project_id"),
inverseJoinColumns = #JoinColumn(name = "users_id"))
private Set<MyUser> projectUserSet = new HashSet<>();
public void addUserToProject(User user) {
this.projectUserSet.add(user);
}
}
Project repo
public interface ProjectRepo extends JpaRepository<Project, Long> { }
User class
// cannot use #Data here because it will cause cyclic ref and stack overflow when accessing userProjectsSet
#Getter
#Setter
#Entity
#Table(name = "users")
public class User {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String firstName;
private String lastName;
#ManyToMany(mappedBy="projectUserSet")
private Set<Project> userProjectsSet = new HashSet<>();
}
User repo
public interface UserRepo extends JpaRepository<User, Long> {}
In app class
#SpringBootApplication
public class MyApplication {
public static void main(String[] args) {
SpringApplication.run(MyApplication.class, args);
}
#Bean
CommandLineRunner run(ProjectRepo projectRepo, EntityManagerFactory entityManagerFactory) {
return args -> {
var testUser = new User();
testUser.setFirstName("first-name");
testUser.setLastName("last-name");
var project = new Project();
project.setName("project-name");
project.setDescription("project-description");
project.addUserToProject(testUser);
projectRepo.save(project);
// get saved user and print some properties
var userInDb = entityManagerFactory.createEntityManager().find(User.class, testUser.getId());
System.out.println(userInDb.getFirstName()); // prints "first-name"
System.out.println(userInDb.getUserProjectsSet().size()); // prints "1"
};
}
}
I have a User Entity.
#Entity
#Table(name = "t_login_user")
public class User extends Auditable<Long> implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#Column(name = "user_id")
private Long id;
#Column(name = "user_uid")
private String userUid;
#Column(name = "user_name")
private String userName;
#OneToOne(fetch = FetchType.EAGER, optional = false)
#JoinColumn(name="primary_role_id")
private Role primaryRole;
#ManyToMany(fetch = FetchType.EAGER)
#JoinTable(name = "t_login_user_role_map", joinColumns = #JoinColumn(name = "user_id"), inverseJoinColumns = #JoinColumn(name = "role_id"))
private List<Role> roles;
}
My Role Entity is
#Entity
#Table(name = "t_login_role")
public class Role extends Auditable<Long> implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#Column(name="role_id")
private Long roleId;
#Column(name="role_code")
private String roleCode;
#ManyToMany(fetch = FetchType.EAGER)
#JoinTable(name = "t_login_role_priv_map", joinColumns = #JoinColumn(name = "role_id"), inverseJoinColumns = #JoinColumn(name = "priv_id"))
private List<Privilege> privileges;
#ManyToMany(fetch = FetchType.EAGER)
#JoinTable(name = "t_login_role_menu_map", joinColumns = #JoinColumn(name = "role_id"), inverseJoinColumns = #JoinColumn(name = "menu_id"))
private List<Menu> menus;
}
My Menu Entity is
#Entity
#Table(name = "t_login_menu")
public class Menu extends Auditable<Long> implements Serializable{
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#Column(name="id")
private Long id;
#Column(name="menu_text")
private String menuText;
#Column(name="menu_icon")
private String menuIcon;
#Column(name="menu_url")
private String menuURL;
}
As you can see my Role has multiple Privileges and Multiple Menus. The problem I face is that when I have a code like
LoggedinUser liu = (LoggedinUser)authentication.getPrincipal();
List<Menu> menus = liu.getPrimaryRole().getMenus();
If I have two privileges say READ_DATA and WRITE_DATA
And three Menus 1. HOME 2.USER 3.PROFILE
my menus variable has a value of [HOME,HOME,USER, USER, PROFILE, PROFILE] (i.e. 2 privileges * 3 Roles)
I suspect that this is due to my Role entity having more than one #ManyToMany annotations.
I tried to search online and Stackoverflow but no results.
Anybody face this issue? Am i doing something fundamentally wrong?
Okay. I understand where the cross join happens. Since both the ManyToMany are being EAGER loaded, this is where the Cross Join Happens.
If I change to LAZY Load then the issue disappears. Slight performance hit on LAZY load, but thats fine since I do it only once and store the result in the session.