Hibernate Composite key problem with partial joining with other entity - spring-boot

Hibernate Composite key problem with partial joining with other entity.
Below code was working for javax.persistence_1.0.0.0_2.0 (Toplink), however same is not working for under Spring Boot (Spring Boot JPA starter - jakarta.persistence-api-2.2.3)
#Table(name = "EMPLOYEE")
#IdClass(EmployeePK.class)
public class Employee implements Serializable {
#Id
#Column(name = "EMP_NUMBER", nullable = false, length = 4000, insertable = false, updatable = false)
private String empNo;
#Id
#Column(name = "RGSN_ID", nullable = false, insertable = false, updatable = false)
private Long registId;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumns( { #JoinColumn(name = "PRJ_ID", referencedColumnName = "PRJ_ID"),
#JoinColumn(name = "EMP_NUMBER",
referencedColumnName = "EMP_NUMBER") })
private Projects projects;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "RGSN_ID")
private Organization organization;
//other fields
//created_by, creation_date, last_update_date, last_updated_by, status
}
Composite Key of Employee Table
public class EmployeePK
implements Serializable {
private String empNo;
private Long registId;
// getter setter equals hashcode
}
Project Table
#Table(name = "PROJECTS")
#IdClass(ProjectsPK.class)
public class Projects implements Serializable {
#Id
#Column(name = "PRJ_ID", nullable = false, insertable = false, updatable = false)
private Long prjId;
#Id
#Column(name = "EMP_NUMBER", nullable = false, length = 4000)
private String empNo;
#OneToMany(mappedBy = "projects")
private List<Employee> empList;
//other fields
//created_by, creation_date, last_update_date, last_updated_by, status
}
Composite keys for Project table
public class ProjectsPK
implements Serializable {
private Long prjId;
private String empNo;
// getter setter equals hashcode
}
Console Exception:
insert
into
EMPLOYEE
(created_by, creation_date, last_update_date, last_updated_by, status, prj_id, emp_number, rgsn_id)
values
(?, ?, ?, ?, ?, ?, ?, ?)
o.h.type.descriptor.sql.BasicBinder : binding parameter [1] as [VARCHAR] - [abc#c.com]
o.h.type.descriptor.sql.BasicBinder : binding parameter [2] as [TIMESTAMP] - [2022-03-16 18:52:37.587915]
o.h.type.descriptor.sql.BasicBinder : binding parameter [3] as [TIMESTAMP] - [2022-03-16 18:52:37.587915]
o.h.type.descriptor.sql.BasicBinder : binding parameter [4] as [VARCHAR] - [abc#c.com]
o.h.type.descriptor.sql.BasicBinder : binding parameter [5] as [VARCHAR] - [A]
o.h.type.descriptor.sql.BasicBinder : binding parameter [6] as [BIGINT] - [435]
o.h.type.descriptor.sql.BasicBinder : binding parameter [7] as [VARCHAR] - [123]
o.h.type.descriptor.sql.BasicBinder : binding parameter [8] as [VARCHAR] - [123]
o.h.type.descriptor.sql.BasicBinder : binding parameter [9] as [BIGINT] - [null]
o.h.engine.jdbc.spi.SqlExceptionHelper : SQL Error: 0, SQLState: S1093
o.h.engine.jdbc.spi.SqlExceptionHelper : The index 9 is out of range.
com.microsoft.sqlserver.jdbc.SQLServerException: The index 9 is out of range.
In above exception it shows two entries for emp_number 123 and because of that index is coming 9.
Not getting what could be the problem also added insertable = false, updatable = false at those entries.

Related

Hibernate Search Sync strategy with Inheritance

I have a Spring boot project with 3 entities, UserEntity which is a standalone and Person which is inherited by lawyer.
I set the automatic indexing strategy to sync, When I insert a new user into the database, the new user is picked immediately, but a new lawyer or person are indexed but the don't appear in the result until I restart the mass indexer.
UserEntity:
#Entity
#Accessors(chain = true)
#Getter
#Setter
#Indexed
#SyncAnnotation(convertor = UserConvertor.class, repoClass = UserDetailServiceImplementation.class)
public class UserEntity implements UserDetails {
#Id
#Basic
#Column(name = "id", columnDefinition = "uniqueidentifier")
#Type(type = "uuid-char")
private UUID id;
#Column(name = "user_name", length = 20)
#Basic
private String username;
#Basic
#Column(name = "email")
#Email
private String email;
#Basic
#Column(name = "full_name", length = 50, nullable = false, columnDefinition = "nvarchar(50)")
#NotNull
#FullTextField(termVector = TermVector.WITH_POSITIONS_OFFSETS)
private String fullName;
PersonEntity:
#Entity
#Accessors(chain = true)
#Getter
#Setter
#Inheritance(strategy = InheritanceType.JOINED)
#DiscriminatorColumn(name = "person_type", discriminatorType = DiscriminatorType.INTEGER)
#DiscriminatorValue("1")
#Indexed
#SyncAnnotation(convertor = ClientConvertor.class, repoClass = PersonRepository.class)
public class PersonEntity implements Serializable {
public PersonEntity(){
this.personType=1;
}
#Id
#Basic
#Column(name = "id", columnDefinition = "uniqueidentifier")
#Type(type = "uuid-char")
private UUID id;
#Basic
#Column(name = "first_name", nullable = false, length = 50, columnDefinition = "nvarchar(50)")
private String firstName;
#Basic
#Column(name = "last_name", nullable = false, length = 50, columnDefinition = "nvarchar(50)")
private String lastName;
#Basic
#Column(name = "father_name", length = 50, columnDefinition = "nvarchar(50)")
private String fatherName;
#Basic
#FullTextField(termVector = TermVector.YES)
#Column(name = "full_name", columnDefinition = "as concat(first_name,' ',isnull(father_name,''),' ',last_name)", insertable = false, updatable = false)
private String fullName;
#Basic
#Column(name = "person_type", insertable = false, updatable = false)
#GenericField
private Integer personType;
And a LawyerEntity that inherits PersonEntity:
#Entity
#Accessors(chain = true)
#Getter
#Setter
#DiscriminatorValue("2")
#Indexed
#SyncAnnotation(convertor = ClientConvertor.class, repoClass = LawyerRepository.class)
public class LawyerEntity extends PersonEntity {
public LawyerEntity(){
this.setPersonType(2);
}
#Basic
#Column(name = "bar_id")
#GenericField
private Integer barId;
#Basic
#Column(name = "bar_card_number")
private Long barCardNumber;
#Basic
#Column(name = "bar_regisration_date")
private LocalDate barRegistrationDate;
#ManyToOne(targetEntity = BarEntity.class)
#JoinColumn(foreignKey = #ForeignKey(name = "fk_lawyer_bar"),
name = "bar_id", referencedColumnName = "id", insertable = false, updatable = false)
#JsonIgnore
private BarEntity bar;
}
When using Sync hibernate search automatic indexing strategy, the UserEntity index updates and includes the newly inserted entities in the index , the TRACE output:
2022-12-22 10:16:06.112 TRACE 68869 --- [nio-8080-exec-4] i.AfterCommitIndexingPlanSynchronization : Processing Transaction's beforeCompletion() phase for org.hibernate.engine.transaction.internal.TransactionImpl#5193eb5f.
2022-12-22 10:16:06.119 TRACE 68869 --- [nio-8080-exec-4] i.AfterCommitIndexingPlanSynchronization : Processing Transaction's afterCompletion() phase for org.hibernate.engine.transaction.internal.TransactionImpl#5193eb5f. Executing indexing plan.
2022-12-22 10:16:06.119 TRACE 68869 --- [nio-8080-exec-4] o.h.s.e.b.o.spi.SingletonTask : Scheduling task 'Lucene indexing orchestrator for index 'User' - 9'.
2022-12-22 10:16:06.120 TRACE 68869 --- [rker thread - 2] o.h.s.e.b.o.spi.SingletonTask : Running task 'Lucene indexing orchestrator for index 'User' - 9'
2022-12-22 10:16:06.120 TRACE 68869 --- [rker thread - 2] o.h.s.e.b.o.spi.BatchingExecutor : Processing 1 works in executor 'Lucene indexing orchestrator for index 'User' - 9'
2022-12-22 10:16:06.132 TRACE 68869 --- [rker thread - 2] o.h.s.e.b.o.spi.BatchingExecutor : Processed 1 works in executor 'Lucene indexing orchestrator for index 'User' - 9'
2022-12-22 10:16:06.132 TRACE 68869 --- [rker thread - 2] o.h.s.e.b.o.spi.SingletonTask : Completed task 'Lucene indexing orchestrator for index 'User' - 9'
However, when entering a new person or a lawyer, the index doesn't reflect the changes, not even after awhile, I need to restart the massindexer for it work, it has a similar output to the previous log, but it doesn't reflect the changes on the index until I restart the mass indexer
2022-12-22 10:14:38.086 TRACE 68869 --- [nio-8080-exec-6] i.AfterCommitIndexingPlanSynchronization : Processing Transaction's beforeCompletion() phase for org.hibernate.engine.transaction.internal.TransactionImpl#6b9d9f5e.
2022-12-22 10:14:38.089 TRACE 68869 --- [nio-8080-exec-6] i.AfterCommitIndexingPlanSynchronization : Processing Transaction's afterCompletion() phase for org.hibernate.engine.transaction.internal.TransactionImpl#6b9d9f5e. Executing indexing plan.
2022-12-22 10:14:38.091 TRACE 68869 --- [nio-8080-exec-6] o.h.s.e.b.o.spi.SingletonTask : Scheduling task 'Lucene indexing orchestrator for index 'Person' - 8'.
2022-12-22 10:14:38.091 TRACE 68869 --- [rker thread - 3] o.h.s.e.b.o.spi.SingletonTask : Running task 'Lucene indexing orchestrator for index 'Person' - 8'
2022-12-22 10:14:38.092 TRACE 68869 --- [rker thread - 3] o.h.s.e.b.o.spi.BatchingExecutor : Processing 1 works in executor 'Lucene indexing orchestrator for index 'Person' - 8'
2022-12-22 10:14:38.137 TRACE 68869 --- [rker thread - 3] o.h.s.e.b.o.spi.BatchingExecutor : Processed 1 works in executor 'Lucene indexing orchestrator for index 'Person' - 8'
2022-12-22 10:14:38.138 TRACE 68869 --- [rker thread - 3] o.h.s.e.b.o.spi.SingletonTask : Completed task 'Lucene indexing orchestrator for index 'Person' - 8'
How can I make it detect show the change in the index without restart mass index ?
I also tried calling hibernate search indexing plan but to no success
I am using Hibernate search 6.1.6.Final with lucene backend and spring boot 2.7.5
As per request:
The code used to search for UserEntity (user can belong to bar):
public List<UserEntity> findInAnyRole(String name, Integer barId, UUID[] role) {
var session = sessionProvider.getSearchSession();
var search = session.search(UserEntity.class);
var res = search.where(f -> f.bool(
b -> {
b.must(f.match().field("fullName").matching(name).fuzzy(2));
if (role != null && role.length > 0) {
b.must(f.bool(b2 -> {
for (var roleValue : role)
b2.should(f.match().field("roles.id").matching(roleValue));
}));
}
if (barId != null)
b.must(f.match().field("barId").matching(barId));
}
));
return res.fetchHits(10);
}
As for PersonEntity:
public List<PersonEntity> findSimilar(#NotNull String name, String[] ids) {
var session = sessionProvider.getSearchSession();
var search = session.search(PersonEntity.class);
var q=search.where(f -> f.bool().must(f.match().field("fullName").matching(name).fuzzy(2))
.must(f.match().field("personType").matching(1))).sort(searchSortFactory -> searchSortFactory.score().desc());
log.info(q.toQuery().queryString());
return q.fetchHits(10);
}
and LawyerEntity:
public List<LawyerEntity> findSimilar(#NotNull String name, Integer barId) {
var session = sessionProvider.getSearchSession();
var search = session.search(LawyerEntity.class);
var res = search.where(f -> f.match().field("fullName").matching(name).fuzzy(2));
if (barId != null)
res = search.where(f -> f.bool().must(f.match().field("fullName").matching(name).fuzzy(2))
.must(f.match().field("barId").matching(barId)));
var list = res.fetchHits(10);
return list;
}
I suspect your problem is here:
#Column(name = "full_name", columnDefinition = "as concat(first_name,' ',isnull(father_name,''),' ',last_name)", insertable = false, updatable = false)
private String fullName;
As you're defining the full name on the database side, it will only be populated correctly when it's loaded from the database. The first time you create your entity, or anytime you change the first name or last name on your Java object, the fullName property in your Java object will not have the correct value, until it's loaded back from the database.
I think that when you create your entity, fullName is null, so Hibernate Search is indexing your entities with a fullName index field set to null, which explains that your queries (with predicates on the fullName field) do not match anything.
When you use the mass indexer, on the other hand, entities are loaded from the database and fullName is populated correctly from the database.
As to solutions, either:
Always update fullName manually whenever you update firstName or lastName. That might be inconvenient.
OR, if you don't need to use fullName in SQL queries, do not persist fullName in the database, do not add a fullName property to your entity, and just declare a getFullName() getter annotated with #javax.persistence.Transient that does the concatenation in Java:
#Transient
#FullTextField(termVector = TermVector.YES)
#IndexingDependency(derivedFrom = {
#ObjectPath(#PropertyValue(propertyName = "firstName")),
#ObjectPath(#PropertyValue(propertyName = "fatherName")),
#ObjectPath(#PropertyValue(propertyName = "lastName"))
})
public String getFullName() {
return firstName
+ ( fatherName == null ? "" : " " + fatherName )
+ " " + lastName;
}
See this section of the documentation for #IndexingDependency.

Spring JPA DataIntegrityViolationException because NotNull column is set to null

I do have an entity OptionGroup with relationships to other entities. One of the relationships is making trouble: An OptionGroup has an owner (User). When I delete an OptionGroup, for some reason, the JPA provider hibernate is trying to set the owner_id of the OptionGroup to null which violates to the NotNull constraint defined for the owner field. I have no clue why hibernate is doing this, but I can see that it is doing this in the log:
2022-08-30 20:17:53.008 DEBUG 17488 --- [nio-8081-exec-1] org.hibernate.SQL : update option_group set description=?, option_group_name=?, owner_id=? where id=?
2022-08-30 20:17:53.008 TRACE 17488 --- [nio-8081-exec-1] o.h.type.descriptor.sql.BasicBinder : binding parameter [1] as [VARCHAR] - [null]
2022-08-30 20:17:53.008 TRACE 17488 --- [nio-8081-exec-1] o.h.type.descriptor.sql.BasicBinder : binding parameter [2] as [VARCHAR] - [Männliche Vornamen]
2022-08-30 20:17:53.008 TRACE 17488 --- [nio-8081-exec-1] o.h.type.descriptor.sql.BasicBinder : binding parameter [3] as [BIGINT] - [null]
2022-08-30 20:17:53.008 TRACE 17488 --- [nio-8081-exec-1] o.h.type.descriptor.sql.BasicBinder : binding parameter [4] as [BIGINT] - [20001]
2022-08-30 20:17:53.012 WARN 17488 --- [nio-8081-exec-1] o.h.engine.jdbc.spi.SqlExceptionHelper : SQL Error: 0, SQLState: 23502
2022-08-30 20:17:53.012 ERROR 17488 --- [nio-8081-exec-1] o.h.engine.jdbc.spi.SqlExceptionHelper : ERROR: NULL value in column »owner_id« of relation »option_group« violates Not-Null-Constraint
If I would have defined cascade delete on the owner field I could imagine that hibernate might delete the owner first, set the owner in the OptionGroup to null and then delete the OptionGroup - although it does not make much sense to first the the owner to null and then delete the OptionGroup...
Do you have any idea why hibernate is setting owner_id to null?
Btw. if I remove the NotNull constraint the behavior is as expected: the OptionGroup is deleted and the User (owner) remains.
This is the OptionGroupClass:
#Entity
#Table(name = "option_group"/*, uniqueConstraints = {
#UniqueConstraint(columnNames = { "owner_id", "option_group_name" }) }*/)
#Getter
#Setter
#NoArgsConstructor
#AllArgsConstructor
public class OptionGroup {
/**
* Id of the Option Group. Generated by the database
*/
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
/**
* Name of the Option Group. Unique in the context of a user.
*/
#NotBlank(message = "Option Group name is mandatory")
#Column(name = "option_group_name")
private String optionGroupName;
/**
* Description for the Option Group
*/
private String description;
/**
* User that is the owner of the Option Group.
*/
#NotNull(message = "Owner cannot be null")
#ManyToOne(fetch = FetchType.LAZY, cascade={CascadeType.PERSIST})
#JoinColumn(name = "ownerId")
private User owner;
/**
* List of options that belong to the Option Group.
*/
#OneToMany(cascade = CascadeType.ALL, mappedBy = "optionGroup", orphanRemoval = true)
#NotEmpty(message = "Options cannot be empty")
private List<Option> options;
/**
* List of invitations that belong to the Option Group.
*/
#OneToMany(cascade = CascadeType.ALL, mappedBy = "optionGroup", orphanRemoval = true)
private List<Invitation> invitations;
#Override
public int hashCode() {
return Objects.hash(description, id, optionGroupName,
options == null ? null : options.stream().map(option -> option.getId()).toList(), owner,
invitations == null ? null : invitations.stream().map(invitation -> invitation.getId()).toList());
}
#Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
OptionGroup other = (OptionGroup) obj;
return Objects.equals(description, other.description) && Objects.equals(id, other.id)
&& Objects.equals(optionGroupName, other.optionGroupName)
&& Objects.equals(options == null ? null : options.stream().map(option -> option.getId()).toList(),
other.options == null ? null : other.options.stream().map(option -> option.getId()).toList())
&& Objects.equals(owner, other.owner)
&& Objects.equals(
invitations == null ? null
: invitations.stream().map(invitation -> invitation.getId()).toList(),
other.invitations == null ? null
: other.invitations.stream().map(invitation -> invitation.getId()).toList());
}
#Override
public String toString() {
return "OptionGroup [id=" + id + ", optionGroupName=" + optionGroupName + ", description=" + description
+ ", owner=" + owner + ", options="
+ (options == null ? null : options.stream().map(option -> option.getId()).toList()) + ", invitations="
+ (invitations == null ? null : invitations.stream().map(invitation -> invitation.getId()).toList())
+ "]";
}
}
As you can see the cascade of owner is limited to persist. f a OptionGroup is created, the owner User is created as well. But if an OptionGroup is deleted the owner User should not be deleted.
This is the User class:
/**
* Entity that represents a user
*
* Primary key: id
*/
#Entity
#Table(name = "usert", uniqueConstraints = {
#UniqueConstraint(columnNames = { "email"}) })
#Getter
#Setter
#NoArgsConstructor
#AllArgsConstructor
public class User {
/**
* Id of the User. Generated by the database
*/
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
/**
* Email address of the invitee.
*/
#Email(message = "Email is not valid", regexp = "(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|\"(?:[\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x21\\x23-\\x5b\\x5d-\\x7f]|\\\\[\\x01-\\x09\\x0b\\x0c\\x0e-\\x7f])*\")#(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[a-z0-9-]*[a-z0-9]:(?:[\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x21-\\x5a\\x53-\\x7f]|\\\\[\\x01-\\x09\\x0b\\x0c\\x0e-\\x7f])+)\\])")
private String email;
/**
* Option Groups of which the user is the owner.
*/
#OneToMany(cascade = CascadeType.ALL, mappedBy = "owner", orphanRemoval = true)
private List<OptionGroup> ownedOptionGroups;
/**
* Invitations of the user.
*/
#OneToMany(cascade = CascadeType.ALL, mappedBy = "invitee", orphanRemoval = true)
private List<Invitation> invitations;
}
And this is the class that triggers the delete
/**
* Service related to Option Groups.
*/
#Service
#Transactional
#AllArgsConstructor
public class OptionGroupService {
/**
* Repository used to access Option Groups.
*/
#Autowired
private OptionGroupRepository optionGroupRepository;
/**
* Deletes the Option Group with the given id.
*
* #param id Id of the Option Group to delete.
* #throws ObjectWithNameDoesNotExistException
* #throws ObjectWithIdDoesNotExistException
*/
public void deleteOptionGroupById(Long id) throws ObjectWithIdDoesNotExistException {
if (optionGroupRepository.existsById(id)) {
optionGroupRepository.deleteById(id);
} else {
throw new ObjectWithIdDoesNotExistException("Option Group", id);
}
}
}
And the repository
public interface OptionGroupRepository extends JpaRepository<OptionGroup, Long> {}
Appreciate your help on this. Thanks.
The root cause was an extensive use of cascades in both, parent and child entities which led to a chain of cascades: by saving an Option Group an Invitation was saved by which again an Option Group was saved. After cleaning this up it works.
I recommend reading: Hibernate - how to use cascade in relations correctly

#GeneratedValue annotation is not working on mapping table

I have a many to many relationship like below.
#Entity
#Table(name = "employees")
public class Employee {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "EMP_ID")
private int id;
private String firstName;
private String lastName;
#JoinTable(name = "employee_project_mapping", joinColumns = #JoinColumn(name = "EMPLOYEE_ID", referencedColumnName = "EMP_ID"), inverseJoinColumns = #JoinColumn(name = "PROJECT_ID", referencedColumnName = "PJT_ID"))
#JsonManagedReference
#ManyToMany(cascade = CascadeType.ALL)
Set<Project> projects = new HashSet<Project>();
.....
.....
}
#Entity
#Table(name = "projects")
public class Project {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "PJT_ID")
private int projectId;
private String projectName;
private int teamSize;
#ManyToMany(mappedBy = "projects")
#JsonBackReference
private Set<Employee> emps = new HashSet<Employee>();
.....
.....
}
#Entity
#Table(name = "employee_project_mapping")
public class EmployeeProjectMapping {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "EMP_PJT_ID")
private Integer empPjtId;
#Column(name = "PROJECT_ID")
private Integer projectId;
#Column(name = "EMPLOYEE_ID")
private Integer emploeeId;
.....
.....
}
But when I am trying to insert an employee object with set of projects, it is failing to create auto generated id for the column EMP_PJT_ID (this is an id to the mapping table record). Can't I add an auto generated id for mapping table using jpa?
Error trace
Hibernate: insert into employees (emp_id, first_name, last_name) values (null, ?, ?)
Hibernate: insert into employee_project_mapping (employee_id, project_id) values (?, ?)
2021-04-22 23:34:25.973 ERROR 24126 --- [nio-8080-exec-9] o.h.engine.jdbc.spi.SqlExceptionHelper : NULL not allowed for column "EMP_PJT_ID"; SQL statement:
insert into employee_project_mapping (employee_id, project_id) values (?, ?) [23502-200]
2021-04-22 23:34:25.975 ERROR 24126 --- [nio-8080-exec-9] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is org.springframework.dao.DataIntegrityViolationException: could not execute statement; SQL [n/a]; constraint [null]; nested exception is org.hibernate.exception.ConstraintViolationException: could not execute statement] with root cause
org.h2.jdbc.JdbcSQLIntegrityConstraintViolationException: NULL not allowed for column "EMP_PJT_ID"; SQL statement:
insert into employee_project_mapping (employee_id, project_id) values (?, ?) [23502-200]
The mapping of a many-to-many should be:
#Entity
#Table(name = "employees")
public class Employee {
...
#OneToMany(mappedBy = "employee", cascade = CascadeType.ALL, orphanRemoval = true)
Set<EmployeeProjectMapping> projects = new HashSet<EmployeeProjectMapping>();
...
// Utility method to update both sides of the association
public void addProject(Project project) {
EmployeeProjectMapping empProject = new PersEmployeeProjectMappingonAddress( this, project );
projects.add( empProject );
project.getEmployees().add( empProject );
}
}
#Entity
#Table(name = "projects")
public class Project {
...
#OneToMany(mappedBy = "project", cascade = CascadeType.ALL, orphanRemoval = true)
private Set<EmployeeProjectMapping> emps = new HashSet<Employee>();
...
}
#Entity
#Table(name = "employee_project_mapping")
public class EmployeeProjectMapping {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "EMP_PJT_ID")
private Integer empPjtId;
#Id
#ManyToOne
#JoinColumn(name = "PROJECT_ID")
private Project project;
#Id
#ManyToOne
#JoinColumn(name = "EMPLOYEE_ID")
private Employee employee;
.....
.....
}
Make sure to check the example on the Hibernate ORM documentation for all the details.

Getting JPA error integrity constraint (FK_XXXXX) violated - parent key not found

I have two tables
CREATE TABLE stripe_product (
id NUMBER NOT NULL PRIMARY KEY,
product_id VARCHAR2(256) NOT NULL,
name VARCHAR2(256) NOT NULL,
description VARCHAR2(256),
active NUMBER(1,0),
deleted NUMBER(1,0),
created_at TIMESTAMP,
created_by NUMBER,
updated_at TIMESTAMP,
updated_by NUMBER,
deleted_at TIMESTAMP,
CONSTRAINT UC_stripe_product_id_product_id UNIQUE (id, product_id),
CONSTRAINT UC_stripe_product_product_id UNIQUE (product_id)
);
And
CREATE TABLE stripe_price (
id NUMBER NOT NULL PRIMARY KEY,
price_id VARCHAR2(256) NOT NULL,
stripe_product_product_id VARCHAR2(256) NOT NULL,
active NUMBER(1,0),
deleted NUMBER(1,0),
currency VARCHAR2(10) NOT NULL,
billing_scheme VARCHAR2(50) NOT NULL,
unit_amount NUMBER NOT NULL,
type VARCHAR2(50) NOT NULL,
recurring_aggregate_usage VARCHAR2(50),
recurring_interval VARCHAR2(50),
recurring_interval_count NUMBER,
recurring_usage_type VARCHAR2(50),
created_at TIMESTAMP,
created_by NUMBER,
updated_at TIMESTAMP,
updated_by NUMBER,
deleted_at TIMESTAMP,
CONSTRAINT UC_stripe_price_id_price_id_stripe_product_product_id UNIQUE (id, price_id, stripe_product_product_id),
CONSTRAINT UC_stripe_price_price_id UNIQUE (price_id),
CONSTRAINT FK_stripe_price_stripe_product_product_id FOREIGN KEY (stripe_product_product_id)
REFERENCES stripe_product(product_id) ON DELETE CASCADE
);
I mapped these tables using the following classes
#Entity
#EntityListeners(AuditingEntityListener.class)
#Table(name = "stripe_product")
public class StripeProduct {
#Id
#SequenceGenerator(name = "stripe_product_seq", sequenceName = "stripe_product_seq", allocationSize = 1)
#GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "stripe_product_seq")
private Long id;
#Column(name = "product_id", unique = true)
private String productId;
#Column(nullable = false)
private String name;
private String description;
private Boolean active;
private Boolean deleted;
#Embedded
private Audit audit = new Audit();
#Column(name = "deleted_at")
private Instant deletedAt;
public StripeProduct() {
}
public StripeProduct(Product product) {
this.productId = product.getId();
this.name = product.getName();
this.description = product.getDescription();
this.active = product.getActive();
this.deleted = product.getDeleted();
}
// getters and setter
}
Other one
#Entity
#EntityListeners(AuditingEntityListener.class)
#Table(name = "stripe_price")
public class StripePrice {
#Id
#SequenceGenerator(name = "stripe_price_seq", sequenceName = "stripe_price_seq", allocationSize = 1)
#GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "stripe_price_seq")
private Long id;
#Column(name = "price_id", unique = true)
private String priceId;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "stripe_product_product_id")
private StripeProduct stripeProduct;
private Boolean active;
private Boolean deleted;
private String currency;
....
#Embedded
private Audit audit = new Audit();
#Column(name = "deleted_at")
private Instant deletedAt;
public StripePrice() {
}
public StripePrice(Price price, StripeProduct stripeProduct) {
Assert.notNull(price, "price cannot be null");
this.priceId = price.getId();
this.stripeProduct = stripeProduct;
this.active = price.getActive();
this.currency = price.getCurrency();
this.billingScheme = price.getBillingScheme();
this.unitAmount = price.getUnitAmount();
this.type = price.getType();
Recurring recurring = price.getRecurring();
if (recurring != null) {
this.recurringAggregateUsage = recurring.getAggregateUsage();
this.recurringInterval = recurring.getInterval();
this.recurringIntervalCount = recurring.getIntervalCount();
this.recurringUsageType = recurring.getUsageType();
}
this.deleted = price.getDeleted();
}
// getters and setters
}
In the database I have the following records
Now if I directly insert the record in the database using the following sql it works
insert into stripe_price (active, created_by, created_at, updated_by, updated_at, billing_scheme,
currency, deleted, deleted_at, price_id, recurring_aggregate_usage, recurring_interval,
recurring_interval_count, recurring_usage_type, stripe_product_product_id, type, unit_amount, id)
values (1, 0, SYSDATE, 0, SYSDATE, 'Billing scheme', 'usd', 0, null, 'adsad', 'hjgjh', 'sfsad', 1,
'asdsad', 'prod_Io2qV0NPORZhnX', 'adsad', 100, 33);
insert into stripe_price (active, created_by, created_at, updated_by, updated_at, billing_scheme,
currency, deleted, deleted_at, price_id, recurring_aggregate_usage, recurring_interval,
recurring_interval_count, recurring_usage_type, stripe_product_product_id, type, unit_amount, id)
values (1, 0, SYSDATE, 0, SYSDATE, 'Billing scheme', 'usd', 0, null, 'price_id-2', 'hjgjh', 'sfsad',
1, 'asdsadxzcxzc', 'prod_Io2qV0NPORZhnX', 'asd1234', 100, 34)
But now using JPA I am getting error
Caused by: java.sql.BatchUpdateException: ORA-02291: integrity constraint (BUILDADMIN.FK_STRIPE_PRICE_STRIPE_PRODUCT_PRODUCT_ID) violated - parent key not found
Here is my code
List<Price> prices = priceCollection.getData();
if (!CollectionUtils.isEmpty(prices)) {
prices.forEach(price -> {
String productId = price.getProduct();
StripeProduct managedStripeProduct = stripeProductRepository.findByProductId(productId).orElse(null);
if (managedStripeProduct != null) {
StripePrice newStripePrice = new StripePrice(price, managedStripeProduct);
StripePrice managedStripePrice = stripePriceRepository.save(newStripePrice);
}
});
}
In debug I found that the following SQL is making
Hibernate: select stripe_price_seq.nextval from dual
Hibernate: insert into stripe_price (active, created_by, created_at, updated_by, updated_at, billing_scheme, currency, deleted, deleted_at, price_id, recurring_aggregate_usage, recurring_interval, recurring_interval_count, recurring_usage_type, stripe_product_product_id, type, unit_amount, id) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
binding parameter [1] as [BIT] - [true]
binding parameter [2] as [BIGINT] - [0]
binding parameter [3] as [TIMESTAMP] - [2021-01-25T23:18:11.104Z]
binding parameter [4] as [BIGINT] - [0]
binding parameter [5] as [TIMESTAMP] - [2021-01-25T23:18:11.104Z]
binding parameter [6] as [VARCHAR] - [per_unit]
binding parameter [7] as [VARCHAR] - [usd]
binding parameter [8] as [BIT] - [null]
binding parameter [9] as [TIMESTAMP] - [null]
binding parameter [10] as [VARCHAR] - [price_1ICQl8JOji9YLkEKmju4jUmu]
binding parameter [11] as [VARCHAR] - [null]
binding parameter [12] as [VARCHAR] - [month]
binding parameter [13] as [BIGINT] - [1]
binding parameter [14] as [VARCHAR] - [licensed]
binding parameter [15] as [BIGINT] - [30]
binding parameter [16] as [VARCHAR] - [recurring]
binding parameter [17] as [BIGINT] - [100000]
binding parameter [18] as [BIGINT] - [80]
As you can notice that there is no stripe_product_product_id when hibernate is making SQL. I think that's why it is generating error.
Although I am setting it on StripePrice but unable to find why I am betting error. Can anyone please explain what I am doing wrong ? And how I can I resolve this issue?
Thanks
I have solved the problem. Actually the problem was I have different name for column. In stripe_product table column name is product_id. While in stripe_price table column name is stripe_product_product_id. So I have to use the following in my mapping
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "stripe_product_product_id", referencedColumnName = "product_id", nullable = false)
private StripeProduct stripeProduct;
So basically referencedColumnName = "product_id" was missing that's why JPA unable to find the product_id value from stripe_product table. Hopefully it will be useful for others too.

Repository GET returns JSON which contains both a content object and content array

I have an entity with a ManyToOne relationship, when I use restTemplate.getForEntity(), the nested entity has its values wrapped in a content field at the same time there is a content array added overriding it
#ManyToOne(fetch = FetchType.LAZY)
#RestResource(exported = false)
#JoinColumn(name = "namespace", nullable = false)
private Namespace namespace;
A GET on the entity with this relation returns the following output body
{
"id" : "some_containing_id",
"alertDefinition" : null,
"namespace" : {
"content" : {
"id" : "some_namespace_id",
"emailSenderName" : "some sender",
"emailSenderId" : "foo#bar.com",
"createdAt" : "2018-07-19T05:24:04.473Z",
"updatedAt" : "2018-07-19T05:24:04.473Z"
},
"content" : [ ],
"links" : [ ]
},
...
So the namespace is being serialized containing 2 content fields with the array replacing the content object containing the values
SpringBoot 2.0.3.RELEASE
Namespace.java
#Entity
#Table(name = "namespace"
, schema = "alert_notification"
)
public class Namespace implements java.io.Serializable {
public transient static final String
EMAIL_SENDER_NAME_DEFAULT = "some sender";
public transient static final String
EMAIL_SENDER_ID_DEFAULT = "foo#vbar.com";
#Id
#Column(name = "id", unique = true, nullable = false)
private String id;
#Builder.Default
#Column(name = "email_sender_name")
private String emailSenderName = EMAIL_SENDER_NAME_DEFAULT;
#Builder.Default
#Column(name = "email_sender_id")
private String emailSenderId = EMAIL_SENDER_ID_DEFAULT;
#CreationTimestamp
#Column(name = "created_at", nullable = false, updatable = false)
private OffsetDateTime createdAt;
#UpdateTimestamp
#Column(name = "updated_at", nullable = false)
private OffsetDateTime updatedAt;
}
For some reason the issue is caused by the Hibernate5 module included in com.fasterxml.jackson.datatype:jackson-datatype-hibernate5 which was recently introduced.
Interestingly enough the module doesn't even need to be enabled, having the dependency on the classpath alone will cause the problem.

Resources