So I want the users to have many notes, and this means that the relation type between users and notes should be OneToMany(meaning one user has many notes). So i have a very strange bug in my application. When create and add the note to the database, and then i also save it in the users it works fine for the first time, however at second try i get the error "Cannot add or update a child row: a foreign key constraint fails". When i add one note to the database it works but when i add another note it gives the same error. I have fixed the bug with the set foreign_key_checks=0 in the database and it works, but it does not work when from my application.
Here are my codes for different classes:
Notes:
#Entity
#Table(name = "notes")
#Getter
#Setter
#ToString
public class Note {
#Id
#Column(name = "note_id")
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#Column(nullable = false)
private String description;
#Temporal(TemporalType.TIMESTAMP)
private Date createdDate = new Date(System.currentTimeMillis());
}
Users:
#Entity
#Table(name = "users")
#Getter
#Setter
#AllArgsConstructor
#NoArgsConstructor
public class User {
#Id
#Column(name = "user_id")
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#Column(nullable = false, unique = true)
private String email;
private String nickname;
private Integer age;
#Column(nullable = false)
private String password;
#ManyToMany(fetch = FetchType.EAGER)
#JoinTable(
name = "users_roles",
joinColumns = #JoinColumn(name = "user_id"),
inverseJoinColumns = #JoinColumn(name = "role_id")
)
private Set<Role> roles = new HashSet<>();
#OneToMany(fetch = FetchType.EAGER)
#JoinColumn(name = "note_id", referencedColumnName = "user_id")
private Set<Note> notes = new HashSet<>();
public void addRole(Role role){
this.roles.add(role);
}
public void addNote(Note note){this.notes.add(note);}
public Set<Note> getNotes(){
return this.notes;
}
}
NoteService:
#Service
public class NoteService {
#Autowired
private NoteRepository noteRepository;
#Autowired
private UserService userService;
public List<Note> getAllNote(){
return noteRepository.findAll();
}
public Note getNote(Long id) throws noteNotFoundException {
Optional<Note> result = noteRepository.findById(id);
if(result.isPresent()){
return result.get();
}
//chveni sheqmnili exeptioni
throw new noteNotFoundException("Could not find any note with given ID: " + id);
}
public void save(Note note) {
noteRepository.save(note);
}
public void deleteNoteById(Long id) throws noteNotFoundException {
if(getNote(id)==null){
throw new noteNotFoundException("Could not find any note with given ID: " + id);
}
noteRepository.deleteById(id);
}
}
UserService:
#Service
public class UserService {
private final Integer USER_ROLE_ID = 1;
private final Long ADMIN_ID = 3L;
#Autowired
private UserRepository repository;
#Autowired
private RoleService roleService;
public List<User> getAllUser(){
return (List<User>) repository.findAll();
}
public List<User> getAllUsersWithoutAdmin(){
List<User> allUsers = repository.findAll();
User admin = repository.getById(ADMIN_ID);
allUsers.remove(admin);
return allUsers;
};
public void save(User u) {
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
String encodedPassword = encoder.encode(u.getPassword());
u.setPassword(encodedPassword);
try {
u.addRole(roleService.getRole(USER_ROLE_ID));
} catch (userNotFoundException e) {
e.printStackTrace();
}
repository.save(u);
}
public void saveNoteToUser(Note note){
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
User user = repository.findByEmail(authentication.getName());
user.addNote(note);
repository.save(user);
}
public Set<Note> getAllNotes(){
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
User user = repository.findByEmail(authentication.getName());
return user.getNotes();
}
public User getUser(Long id) throws userNotFoundException {
Optional<User> result = repository.findById(id);
if(result.isPresent()){
return result.get();
}
//chveni sheqmnili exeptioni
throw new userNotFoundException("Could not find any user with given ID: " + id);
}
public void deleteUserById(Long id) throws userNotFoundException {
//eseigi jer itvli tu 0 ia an null errors abruen
//tu useri arsebobs mere adeleteb
Long count = repository.countById(id);
if(count == null || count==0){
throw new userNotFoundException("Could not find any user with given ID: " + id);
}
repository.deleteById(id);
}
}
And Finally Mapping:
#PostMapping("/notes/save")
public String saveNote(Note note, RedirectAttributes ra){
noteService.save(note);
userService.saveNoteToUser(note);
//ra atributi ari roca redirect moxdeba mere ro dawers messijs anu redirectis mere xdeba
ra.addFlashAttribute("message", "The note has been added successfully.");
return "redirect:/notes";
}
In mapping as you can see, firstly i am trying to save a note in the database, and after that i want to add that note to the user itself. However as mentioned above it works only once, however when i want to add another note i get this error:
java.sql.SQLIntegrityConstraintViolationException: Cannot add or update a child row: a foreign key constraint fails (`myfirstappdb`.`notes`, CONSTRAINT `FKb7tumg0c2p1wt2ifjag2gv998` FOREIGN KEY (`note_id`) REFERENCES `users` (`user_id`))
at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:117) ~[mysql-connector-java-8.0.27.jar:8.0.27]
at com.mysql.cj.jdbc.exceptions.SQLExceptionsMapping.translateException(SQLExceptionsMapping.java:122) ~[mysql-connector-java-8.0.27.jar:8.0.27]
at com.mysql.cj.jdbc.ClientPreparedStatement.executeInternal(ClientPreparedStatement.java:953) ~[mysql-connector-java-8.0.27.jar:8.0.27]
at com.mysql.cj.jdbc.ClientPreparedStatement.executeUpdateInternal(ClientPreparedStatement.java:1098) ~[mysql-connector-java-8.0.27.jar:8.0.27]
at com.mysql.cj.jdbc.ClientPreparedStatement.executeUpdateInternal(ClientPreparedStatement.java:1046) ~[mysql-connector-java-8.0.27.jar:8.0.27]
at com.mysql.cj.jdbc.ClientPreparedStatement.executeLargeUpdate(ClientPreparedStatement.java:1371) ~[mysql-connector-java-8.0.27.jar:8.0.27]
at com.mysql.cj.jdbc.ClientPreparedStatement.executeUpdate(ClientPreparedStatement.java:1031) ~[mysql-connector-java-8.0.27.jar:8.0.27]
at com.zaxxer.hikari.pool.ProxyPreparedStatement.executeUpdate(ProxyPreparedStatement.java:61) ~[HikariCP-4.0.3.jar:na]
at com.zaxxer.hikari.pool.HikariProxyPreparedStatement.executeUpdate(HikariProxyPreparedStatement.java) ~[HikariCP-4.0.3.jar:na]
According to the Hibernate Documentation when you have a UniDirectional relation in your entity schema and you have only the side of #OneToMany , you can't use the annotation #JoinColumn.
According to the Doc
When using a unidirectional #OneToMany association, Hibernate resorts
to using a link table between the two joining entities.
You must remove the #JoinColumn so that hibernate follows the default process of creating a intermediate join table and then it will be able to proceed.
Related
I'm trying to integrate sorting with Pageable on joined fields with the use of #Query annotation from Spring Data.
1st interface's method (without #Query but with the Pageable) works like a charm. Same like when I'm fetching only one Employee with the #Query but instead of Pageable I'm using Optional<Employee> there (3rd method). But the fun begins when I try to put these two all together in one - it won't work anymore.
When I try to sort the data by name field it screams with this error:
Caused by: org.hibernate.QueryException: could not resolve property: name of: (....).model.employee.Employee
So the question is: how to tell spring to look for name in joined fields? How to do this with Spring Data?
I've already tried several things but they didn't work or I still don't know how to use them properly:
someone suggested to add countQuery to the #Query parameters so this corresponds somehow with the pagination (spring data jpa #query and pageable)
I've followed Baeldung's tutorial but this doesn't cover joins
Spring-Data FETCH JOIN with Paging is not working also suggested using countQuery but I'd prefer to stick to Page<Employee> rather than List<Employee>.
I'll leave some samples of the code below. Feel free to ask for update if I omitted something important.
// Employee
#Entity
#Table(name = "employee", schema = "emp")
#Data
#NoArgsConstructor
public class Employee {
private static final String SEQUENCE = "EMPLOYEE_SEQUENCE";
#Id
#SequenceGenerator(sequenceName = SEQUENCE, name = SEQUENCE, allocationSize = 1)
#GeneratedValue(strategy = GenerationType.SEQUENCE, generator = SEQUENCE)
private Long id;
#Column(name = "employee_number")
private String employeeNumber;
#Column
#Enumerated(EnumType.STRING)
private EmployeeStatus status;
#OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
#Audited(targetAuditMode = RelationTargetAuditMode.NOT_AUDITED)
#JoinColumn(name = "id_details")
private Details details;
// some other fields ...
}
// Details
#Entity
#Table(name = "details", schema = "emp")
#Data
#NoArgsConstructor
public class Details {
private static final String SEQUENCE = "DETAILS_SEQUENCE";
#Id
#SequenceGenerator(sequenceName = SEQUENCE, name = SEQUENCE, allocationSize = 1)
#GeneratedValue(strategy = GenerationType.SEQUENCE, generator = SEQUENCE)
private Long id;
private String name;
private String surname;
// some other fields ...
}
// EmployeeDTO
#NoArgsConstructor
#AllArgsConstructor
#Data
#Builder(toBuilder = true)
public class EmployeeDTO {
private Long id;
private String employeeNumber;
private String status;
private String name;
private String surname;
// some other fields ...
}
// EmployeeRepository
#Repository
public interface EmployeeRepository extends JpaRepository<Employee, Long> {
// 1st method
Page<Employee> findByStatus(EmployeeStatus status, Pageable pageable);
// 2nd method
#Query(value = "select e from Employee e join e.details where e.status = :status",
countQuery = "select count(*) from Employee e join e.details where e.status = :status")
Page<Employee> getEmployeeDetails(#Param("status") EmployeeStatus status, Pageable pageable);
// 3rd method
#Query("select e from Employee e join fetch e.details where e.id = :id")
Optional<Employee> findByIdWithDetails(Long id);
// ...
}
// EmployeeService
#Service
public class EmployeeService {
private final EmployeeRepository employeeRepository;
private final EntityDtoConverter entityDtoConverter;
#Autowired
public EmployeeService(EmployeeRepository employeeRepository, EntityDtoConverter entityDtoConverter) {
this.employeeRepository = employeeRepository;
this.entityDtoConverter = entityDtoConverter;
}
public EmployeeResponse getEmployeesByStatus(EmployeeStatus status, int pageSize, int pageIndex, Sort.Direction sortDirection, String sortColumn) {
Page<EmployeeDTO> employeePage = employeeRepository.findByStatus(status, PageRequest.of(pageIndex, pageSize, Sort.by(sortDirection, sortColumn)))
.map(entityDtoConverter::convertEmployeeBaseToDto);
return new EmployeeResponse(employeePage);
}
public EmployeeResponse getEmployeeDetails(EmployeeStatus status, int pageSize, int pageIndex, Sort.Direction sortDirection, String sortColumn) {
Page<EmployeeDTO> employeePage = employeeRepository.getEmployeeDetails(status, PageRequest.of(pageIndex, pageSize, Sort.by(sortDirection, sortColumn)))
.map(entityDtoConverter::convertToEmployeeWithDetailsDto);
return new EmployeeResponse(employeePage);
}
// ...
}
// EntityDtoConverter
#Component
public class EntityDtoConverter {
public EmployeeDTO convertEmployeeBaseToDto(Employee entity) {
return EmployeeDTO.builder()
.id(entity.getId())
.employeeNumber(entity.getEmployeeNumber())
.status(entity.getStatus())
.build();
}
public EmployeeDTO convertToEmployeeWithDetailsDto(Employee entity) {
return convertEmployeeBaseToDto(entity).toBuilder()
.name(entity.getDetails().getName())
.surname(entity.getDetails().getSurname())
.build();
}
// ...
}
EDIT:
This is one of the methods of my rest controller:
#GetMapping
public ResponseEntity<EmployeeResponse> getEmployeesByStatus(EmployeeStatus status, int pageSize, int pageIndex, String sortDirection, String sortColumn) {
try {
EmployeeResponse employeeResponse = employeeService.getEmployeesByStatus(status, pageSize, pageIndex, Sort.Direction.fromString(sortDirection), sortColumn);
return employeeResponse.getTotalElements().equals(0L) ? ResponseEntity.noContent().build() : ResponseEntity.ok(employeeResponse);
} catch (Exception e) {
log.error(ERROR_MESSAGE, e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}
}
Try below code.
Specification<Employee> joins = (employee, query, cb) -> {
Join<Employee, Detail> details = employee.join("details");
return cb.and(
employee.equal(employee.get("name", name)),
details.equal(details.get("name", detailName))
);
};
PageRequest pageRequest = new PageRequest(0, 2, new Sort(Sort.Direction.DESC, "name"));
Page<Employee> customerPage = employeeRepository.findAll(joins, pageRequest);
Here we are trying to inform JPA that this name is foreign key for Employee table.
I have a class CompanyUser.java which acts as an inner table for a many to many relation on Company and User. Now I would like to update a value (privilege level) but when doing so I get this error back. I checked all getters and setters. My CompanyUser.java class looks like the following.
#Entity
public class CompanyUser
{
#EmbeddedId
private CompanyUserId id;
#ManyToOne(fetch = FetchType.EAGER, cascade = CascadeType.ALL)
#MapsId("companyId")
private Company company;
#ManyToOne(fetch = FetchType.EAGER, cascade = CascadeType.ALL)
#MapsId("userId")
private User user;
#Column(name = "privilege_level")
private Integer privilegeLevel = CompanyPrivilegeLevel.DEFAULT.getValue();
public CompanyUser() {}
public CompanyUser(Company company, User user, Integer priveligeLevel)
{
this.company = company;
this.user = user;
this.privilegeLevel = priveligeLevel;
this.id = new CompanyUserId(user.getId(), company.getId());
}
... Getters and Setters ...
}
The embedded CompanyUserId.java:
#Embeddable
public class CompanyUserId implements Serializable
{
#Column(name = "user_id")
private int userId;
#Column(name = "company_id")
private int companyId;
public CompanyUserId() {}
public CompanyUserId(int userId, int companyId)
{
this.userId = userId;
this.companyId = companyId;
}
... Getters and setters & equals and hashCode method ...
}
A nullPointer exception is thrown so I tried changing
#EmbeddedId private CompanyUserId id;
to
#EmbeddedId private CompanyUserId id = new CompanyUserId();
But when I do this I get this "Hibernate Error: a different object with the same identifier value was already associated with the session" error.
Thanks in advance.
Exception thrown:
java.lang.NullPointerException: null
at java.base/jdk.internal.reflect.UnsafeFieldAccessorImpl.ensureObj(UnsafeFieldAccessorImpl.java:57) ~[na:na]
at java.base/jdk.internal.reflect.UnsafeIntegerFieldAccessorImpl.set(UnsafeIntegerFieldAccessorImpl.java:75) ~[na:na]
at java.base/java.lang.reflect.Field.set(Field.java:780) ~[na:na]
at org.springframework.orm.jpa.ExtendedEntityManagerCreator$ExtendedEntityManagerInvocationHandler.invoke(ExtendedEntityManagerCreator.java:368) ~[spring-orm-5.2.1.RELEASE.jar:5.2.1.RELEASE]
at org.springframework.orm.jpa.SharedEntityManagerCreator$SharedEntityManagerInvocationHandler.invoke(SharedEntityManagerCreator.java:314) ~[spring-orm-5.2.1.RELEASE.jar:5.2.1.RELEASE]
at com.ewa.urbananalytics.ewabackend.repository.JpaCompanyUserRepository.save(JpaCompanyUserRepository.java:22) ~[classes/:na]
EDIT:
Thanks to #locus2k, I managed to get it working! There was something wrong with the way I was retrieving the object. I don't think it is the most efficient way but for now, it works.
#PostMapping("/company/user/save")
#Transactional
public void saveCompanyUser(#RequestBody CompanyUser cu)
{
CompanyUserId companyUserId = new CompanyUserId(cu.getUser().getId(), cu.getCompany().getId());
CompanyUser foundCU = companyUserRepository.findById(companyUserId);
foundCU.setPriveligeLevel(cu.getPrivilegeLevel());
companyUserRepository.save(foundCU);
}
It looks like you're not retrieving your CompanyUser correctly for updates.
What you have to do is set the values of the CompanUserId you want to update first then query off that object, update the results then persist it.
If you're using SpringRepository you can do something like:
CompanyUserId id = new CompanyUserId(1, 1);
CompanyUser user = repo.findById(id);
user.setPrivilegeLevel(1);
repo.save(user);
just make sure you do your necessary error handling.
If you're using hibernate for queries your query would look like:
String query = "SELECT cu FROM CompanyUser cu WHERE cu.id = :id";
CompanyUserId id = new CompanyUserId(1, 1);
TypedQuery<CompanyUser> q = entityManager.createQuery(query, CompanyUser.class);
q.setParameter("id", id);
CustomerUser user = q.getSingleResult();
user.setPrivilegeLevel(1);
entityManager.persist(user);
Make sure to remove #EmbeddedId private CompanyUserId id = new CompanyUserId(); and set it back to just #EmbeddedId private CompanyUserId id;
Hibernate 4.3.11
I have an issue saving the following object graph in hibernate. The Employer is being saved using the merge() method.
Employer
|_ List<EmployerProducts> employerProductsList;
|_ List<EmployerProductsPlan> employerProductsPlan;
The Employer & EmployerProducts have a auto generated pk. The EmployerProductsPlan is a composite key consisting of the EmployerProducts id and a String with the plan code.
The error occurs when there is a transient object in the EmployerProducts list that cascades to List<EmployerProductsPlan>. The 1st error that I encountered which I have been trying to get past was an internal hibernate NPE. This post here perfectly describes the issue that I am having which causes the null pointer Hibernate NullPointer on INSERTED id when persisting three levels using #Embeddable and cascade
The OP left a comment specifying what they did to resolve, but I end up with a different error when changing to the suggested mapping. After changing the mapping, I am now getting
org.hibernate.NonUniqueObjectException: A different object with the same identifier value was already associated with the session : [com.webexchange.model.EmployerProductsPlan#com.webexchange.model.EmployerProductsPlanId#c733f9bd]
Due to other library dependencies, I cannot upgrade above 4.3.x at this time. This project is using spring-boot-starter-data-jpa 1.3.3. No other work is being performed on the session other than calling merge() and passing the employer object.
Below is the mappings for each class:
Employer
#Entity
#Table(name = "employer")
#lombok.Getter
#lombok.Setter
#lombok.EqualsAndHashCode(of = {"employerNo"})
public class Employer implements java.io.Serializable {
#Id
#GeneratedValue(strategy = IDENTITY)
#Column(name = "EMPLOYER_NO", unique = true, nullable = false)
private Long employerNo;
.....
#OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY, mappedBy = "employer", orphanRemoval = true)
private List<EmployerProducts> employerProductsList = new ArrayList<>(0);
}
EmployerProducts
#Entity
#Table(name = "employer_products")
#Accessors(chain = true) // has to come before #Getter and #Setter
#lombok.Getter
#lombok.Setter
#lombok.EqualsAndHashCode(of = {"employerProductsNo"})
public class EmployerProducts implements Serializable {
#Id
#GeneratedValue(strategy = IDENTITY)
#Column(name = "employer_products_no", unique = true, nullable = false)
private Long employerProductsNo;
#ManyToOne
#JoinColumn(name = "employer_no", nullable = false)
private Employer employer;
......
#OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY, mappedBy = "employerProducts", orphanRemoval = true)
private List<EmployerProductsPlan> employerProductsPlanList = new ArrayList<>(0);
}
EmployerProductsPlan
#Accessors(chain = true) // has to come before #Getter and #Setter
#lombok.Getter
#lombok.Setter
#lombok.EqualsAndHashCode(of = {"id"})
#Entity
#Table(name="employer_products_plan")
public class EmployerProductsPlan implements Serializable {
#EmbeddedId
#AttributeOverrides({ #AttributeOverride(name = "plan", column = #Column(name = "epp_plan", nullable = false)),
#AttributeOverride(name = "employerProductsNo", column = #Column(name = "employer_products_no", nullable = false)) })
private EmployerProductsPlanId id;
#ManyToOne
#JoinColumn(name = "employer_products_no")
#MapsId("employerProductsNo")
private EmployerProducts employerProducts;
}
I am populating the employerProducts above with the same instance of the EmployerProducts object that is being saved. It is transient and has no id populated as it does not existing in the db yet.
EmployerProductsPlanId
#Accessors(chain = true) // has to come before #Getter and #Setter
#lombok.Getter
#lombok.Setter
#lombok.EqualsAndHashCode(of = {"plan", "employerProductsNo"})
#Embeddable
public class EmployerProductsPlanId implements Serializable {
private String plan;
private Long employerProductsNo;
// This was my previous mapping that was causing the internal NPE in hibernate
/* #ManyToOne
#JoinColumn(name = "employer_products_no")
private EmployerProducts employerProducts;*/
}
UPDATE:
Showing struts controller and dao. The Employer object is never loaded from the db prior to the save. Struts is creating this entire object graph from the Http request parameters.
Struts 2.5 controller
#lombok.Getter
#lombok.Setter
public class EditEmployers extends ActionHelper implements Preparable {
#Autowired
#lombok.Getter(AccessLevel.NONE)
#lombok.Setter(AccessLevel.NONE)
private IEmployerDao employerDao;
private Employer entity;
....
public String save() {
beforeSave();
boolean newRecord = getEntity().getEmployerNo() == null || getEntity().getEmployerNo() == 0;
Employer savedEmployer = newRecord ?
employerDao.create(getEntity()) :
employerDao.update(getEntity());
setEntity(savedEmployer);
return "success";
}
private void beforeSave() {
Employer emp = getEntity();
// associate this employer record with any products attached
for (EmployerProducts employerProduct : emp.getEmployerProductsList()) {
employerProduct.setEmployer(emp);
employerProduct.getEmployerProductsPlanList().forEach(x ->
x.setEmployerProducts(employerProduct));
}
// check to see if branding needs to be NULL. It will create the object from the select parameter with no id
// if a branding record has not been selected
if (emp.getBranding() != null && emp.getBranding().getBrandingNo() == null) {
emp.setBranding(null);
}
}
}
Employer DAO
#Repository
#Transactional
#Service
#Log4j
public class EmployerDao extends WebexchangeBaseDao implements IEmployerDao {
private Criteria criteria() {
return getCurrentSession().createCriteria(Employer.class);
}
#Override
#Transactional(readOnly = true)
public Employer read(Serializable id) {
return (Employer)getCurrentSession().load(Employer.class, id);
}
#Override
public Employer create(Employer employer) {
getCurrentSession().persist(employer);
return employer;
}
#Override
public Employer update(Employer employer) {
getCurrentSession().merge(employer);
return employer;
}
}
As of right now, my solution is to loop through the EmployerProducts and check for new records. I called a persist on the new ones before calling the merge() on the parent Employer. I also moved the logic I had associating all the keys into the dao instead of having it in my Struts action. Below is what my update() method in the Employer DAO now looks like
public Employer update(Employer employer) {
// associate this employer record with any products attached
for (EmployerProducts employerProduct : employer.getEmployerProductsList()) {
employerProduct.setEmployer(employer);
if (employerProduct.getEmployerProductsNo() == null) {
// The cascade down to employerProductsPlanList has issues getting the employerProductsNo
// automatically if the employerProduct does not exists yet. Persist the new employer product
// before we try to insert the new composite key in the plan
// https://stackoverflow.com/questions/54517061/hibernate-4-3-cascade-merge-through-multiple-lists-with-embeded-id
List<EmployerProductsPlan> plansToBeSaved = employerProduct.getEmployerProductsPlanList();
employerProduct.setEmployerProductsPlanList(new ArrayList<>());
getCurrentSession().persist(employerProduct);
// add the plans back in
employerProduct.setEmployerProductsPlanList(plansToBeSaved);
}
// associate the plan with the employer product
employerProduct.getEmployerProductsPlanList().forEach(x ->
x.getId().setEmployerProductsNo(employerProduct.getEmployerProductsNo())
);
}
return (Employer)getCurrentSession().merge(employer);
}
I am working on an authentication model that would suit GlassFish's JDBC Realm requirements.
In this model I have one group which can contain multiple users, but each user can only be in one group (e.g. su, admin, etc.).
I have two entities: Groups.java (for groups) and Credential.java (for users) and intend to feed the generated join table to Glassfish's "Group Table" property.
I am able to persist both Groups and Credential instances, but the required middle table (credential_groups) is not even created, let alone updated.
Below are my entities:
Credential.java:
#Entity
#Access(AccessType.PROPERTY)
#Table(name = "credential")
public class Credential extends MetaInfo implements Serializable {
private String username;
private String passwd;
private Groups group;
private boolean blocked;
private boolean suspended;
public Credential() {
super();
}
public Credential(String createdBy) {
super(Instant.now(), createdBy);
}
public Credential(String createdBy, String username, String passwd) {
this(createdBy);
this.username = username;
this.passwd = passwd;
}
#Id
#Column(name = "username")
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
updateModified();
}
#Column(name = "passwd")
public String getPasswd() {
return passwd;
}
public void setPasswd(String passwd) {
this.passwd = passwd;
updateModified();
}
#ManyToOne(fetch = FetchType.LAZY)
public Groups getGroups() {
return group;
}
public void setGroups(Groups group) {
this.group = group;
group.getCredentials().add(this);
updateModified();
}
#Column(name = "is_blocked")
public boolean isBlocked() {
return blocked;
}
public void setBlocked(boolean blocked) {
this.blocked = blocked;
updateModified();
}
#Column(name = "is_suspended")
public boolean isSuspended() {
return suspended;
}
public void setSuspended(boolean suspended) {
this.suspended = suspended;
updateModified();
}
}
Groups.java:
#Entity
#Access(AccessType.PROPERTY)
#Table(name = "groups")
#NamedQueries({
#NamedQuery(name = "findAllGroups",
query = "SELECT g FROM Groups g order by g.modifiedDate DESC")
})
public class Groups extends MetaInfo implements Serializable {
private String groupName;
private Set<Credential> credentials;
public Groups() {
super();
credentials = new HashSet();
}
public Groups(String groupName) {
this();
this.groupName = groupName;
}
public Groups(String createdBy, String groupName) {
this();
setCreatedBy(createdBy);
this.groupName = groupName;
}
#Id
#Column(name = "group_name")
public String getGroupName() {
return groupName;
}
public void setGroupName(String groupName) {
this.groupName = groupName;
updateModified();
}
#OneToMany(mappedBy = "groups")
#JoinTable(
name = "credential_groups",
joinColumns = #JoinColumn(name = "group_name"),
inverseJoinColumns = #JoinColumn(name = "username")
)
public Set<Credential> getCredentials() {
return credentials;
}
public void setCredentials(Set<Credential> credentials) {
this.credentials = credentials;
}
public void addCredential(Credential c) {
credentials.add(c);
if (c.getGroups() != this) {
c.setGroups(this);
}
}
}
As I said persisting both works (for Groups I have also tried updating, querying), but this error keeps on firing on every operation with either entity:
Internal Exception: com.mysql.jdbc.exceptions.jdbc4.MySQLIntegrityConstraintViolationException: Can't write; duplicate key in table '#sql-4d2_54'
Error Code: 1022
Call: ALTER TABLE credential ADD CONSTRAINT FK_credential_GROUPS_group_name FOREIGN KEY (GROUPS_group_name) REFERENCES groups (group_name)
Query: DataModifyQuery(sql="ALTER TABLE credential ADD CONSTRAINT FK_credential_GROUPS_group_name FOREIGN KEY (GROUPS_group_name) REFERENCES groups (group_name)")
Important update:
This is the error that is being thrown even before the pasted above exception:
Internal Exception: com.mysql.jdbc.exceptions.jdbc4.MySQLSyntaxErrorException: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'group VARCHAR(255) NOT NULL, PRIMARY KEY (credential, group))' at line 1
Error Code: 1064
Call: CREATE TABLE credential_groups (credential VARCHAR(255) NOT NULL, group VARCHAR(255) NOT NULL, PRIMARY KEY (credential, group))
As requested by Francois Motard, here's my jUnit method that triggers the error (the group is created in another test class):
public class CredentialRepositoryTests {
private final OCGroupsRepository groupRepo;
private final OCCredentialRepository credentialRepo;
public CredentialRepositoryTests() {
groupRepo = new OCGroupsRepository();
credentialRepo = new OCCredentialRepository();
}
...
#Test
public void createCredentialTest(){
//retrieve the group
Groups admin = groupRepo.getByGroupName("admin");
Credential rimma = new Credential("user_creator", "sample_user", "abc123");
admin.addCredential(rimma);
assertTrue(groupRepo.updateGroups(admin));
}
I based my code on the instructions from the EJB 3.0 in Action by Manning and tried to resolve the join table issue based on this stackoverflow answer: How to create join table with JPA annotations?
Can anyone help me have the join table created and updated? Thank you very much.
Resolved by removing the mappedBy attribute from the #OneToMany annotation of the owned entity (this fixed the non-creation of the join table) and by adding the unique attribute (set to true - this solved the "PRIMARY KEY (credential, group)" issue) to the #JoinColumn annotation inside the #JoinTable of the same property:
#OneToMany
#JoinTable(
name = "credential_groups",
joinColumns = #JoinColumn(name = "group_name"),
inverseJoinColumns = #JoinColumn(name = "username", unique=true)
)
public Set<Credential> getCredentials() {
return credentials;
}
Main take away don't ever ever combine a "mappedBy" with the #JoinTable in a ManyToOne/OneToMany entities pair.
I am having some problems with JPA. I am new at this topic so my question maybe is really stupid, but i hope some of you could point me to the right direction.
I have Project and User entity. Every user can have as many projects assign to him as it can.
I created the relationship bidirectional
User OneToMany -> Project,
Project ManyToOne -> User
My problem is that if i want to delete a user i want all the projects to be deleted as well, but i receive an error at that point:
Internal Exception: java.sql.SQLIntegrityConstraintViolationException:
DELETE on table 'USER_DATA' caused a violation of
foreign key constraint 'PROJECT_USERNAME' for key (Test Use1312r1).
The statement has been rolled back.
Error Code: -1
Call: DELETE FROM USER_DATA WHERE (USERNAME = ?)
bind => [1 parameter bound]
My User entity looks like this:
#Entity
#Table(name="USER_DATA", uniqueConstraints = #UniqueConstraint(columnNames = {"USERNAME", "link"}))
public class User implements Serializable {
#Column(name="USERNAME")
#Id
#NotNull
private String name;
#Column(name="USERROLE")
#Enumerated(EnumType.STRING)
private UserRole role;
private String password;
private String link;
// Should be unique
private String session;
#OneToMany(mappedBy="user", cascade=CascadeType.ALL)
private Collection<Project> projects;
My Project Entity like this:
#Entity
#Table(name="PROJECT")
#XmlRootElement
public class Project implements Serializable {
#Id
private int id;
private String name;
private String description;
#Temporal(TemporalType.TIMESTAMP)
#Column(name="START_DATE")
private Date beginDate;
#Temporal(TemporalType.TIMESTAMP)
#Column(name="END_DATE")
private Date endDate;
#ManyToOne
#JoinColumn(name="USERNAME", nullable=false,updatable= true)
private User user;
And my BL:
public User getUser(String userName) throws NoDataFoundException {
EntityManager em = DbConnection.getInstance().getNewEntity();
try {
User user = em.find(User.class, userName);
if (user == null) {
throw new NoDataFoundException("User is not found in the DB");
}
return user;
} finally {
em.close();
}
}
public void deleteUser(String userName) throws ModelManipulationException {
EntityManager em = DbConnection.getInstance().getNewEntity();
try {
User userToBeDeleted = getUser(userName);
em.getTransaction().begin();
userToBeDeleted = em.merge(userToBeDeleted);
em.remove(userToBeDeleted);
em.getTransaction().commit();
} catch (Exception e) {
throw new ModelManipulationException(
"Error in deleting user data for username" + userName
+ "with exception " +e.getMessage(),e);
}
finally{
em.close();
}
}
Thanks in advance guys.
after the merge call, are there any Projects in userToBeDeleted.projects? I suspect there are none, which prevents any from being deleted. Cascade remove can only work if you populate both sides of bidirectional relationships, so check that when you associate a user to a project, you also add the project to the user's project collection.