Hibernate Search Sync strategy with Inheritance - spring-boot

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.

Related

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

Hibernate Composite key problem with partial joining with other entity

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.

Error while indexing in Hibernate Search - Could not get property value

I am using Hibernate Search with Spring Boot to create a searchable rest api. Trying to POST an instance of "Training", I receive the following stack traces. None of the two are very insightful to me which is why I am reaching out for help.
Stack trace:
https://pastebin.com/pmurg1N3
It appears to me that it is trying to index a null entity!? How can that happen? Any ideas?
The entity:
#Entity #Getter #Setter #NoArgsConstructor
#ToString(onlyExplicitlyIncluded = true)
#Audited #Indexed(index = "Training")
#AnalyzerDef(name = "ngram",
tokenizer = #TokenizerDef(factory = StandardTokenizerFactory.class ),
filters = {
#TokenFilterDef(factory = StandardFilterFactory.class),
#TokenFilterDef(factory = LowerCaseFilterFactory.class),
#TokenFilterDef(factory = StopFilterFactory.class),
#TokenFilterDef(factory = NGramFilterFactory.class,
params = {
#Parameter(name = "minGramSize", value = "2"),
}
)
}
)
#Analyzer(definition = "ngram")
public class Training implements BaseEntity<Long>, OwnedEntity {
#Id
#GeneratedValue
#ToString.Include
private Long id;
#NotNull
#RestResourceMapper(context = RestResourceContext.IDENTITY, path = "/companies/{id}")
#JsonProperty(access = Access.WRITE_ONLY)
#JsonDeserialize(using = RestResourceURLSerializer.class)
private Long owner;
#NotNull
#Field(index = Index.YES, analyze = Analyze.YES, store = Store.YES)
private String name;
#Column(length = 10000)
private String goals;
#Column(length = 10000)
private String description;
#Enumerated(EnumType.STRING)
#Field(index = Index.YES, store = Store.YES, analyze = Analyze.NO, bridge=#FieldBridge(impl=EnumBridge.class))
private Audience audience;
#Enumerated(EnumType.STRING)
#Field(index = Index.YES, store = Store.YES, analyze = Analyze.NO, bridge=#FieldBridge(impl=EnumBridge.class))
private Level level;
#ManyToMany
#Audited(targetAuditMode = RelationTargetAuditMode.NOT_AUDITED)
#NotNull #Size(min = 1)
#IndexedEmbedded
private Set<ProductVersion> versions;
#NotNull
private Boolean enabled = false;
#NotNull
#Min(1)
#IndexedEmbedded
#Field(index = Index.YES, store = Store.YES, analyze = Analyze.NO)
#NumericField
private Integer maxStudents;
#NotNull
#ManyToOne(fetch = FetchType.LAZY)
private Agenda agenda;
#NotNull
#Min(1)
#Field(index = Index.YES, store = Store.YES, analyze = Analyze.NO)
#NumericField
private Integer durationDays;
#IndexedEmbedded
#Audited(targetAuditMode = RelationTargetAuditMode.NOT_AUDITED)
#ManyToMany(cascade = CascadeType.PERSIST)
private Set<Tag> tags = new HashSet<>();
I'd say either your versions collection or your tags collection contains null objects, which is generally not something we expect in a Hibernate ORM association, and apparently not something Hibernate Search expects either.
Can you check that in debug mode?

Why lazy init entity is null when assigned to DTO inside transaction boundary?

I am porting my legacy code to spring boot + angular application, which was spring + angular js earlier. My pojo has #ManyToOne relationship in it which is lazy.
When i try to create a DTO object from original object, the child object inside orginal object is null, when sent to client. In my legacy application same code works perfectly.
If i make it eager or i call sysout on that child element before creating DTO then it works, probably because getters of child object is called internally.
Parent object
public class ComponentInfo implements Serializable{
private static final long serialVersionUID = -1135710750835719391L;
#Id
#Column(name="TAGGING_KEY")
private String taggingKey;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "COMPONENT_CONFIG_ID",nullable = true)
private ReportComponentConfig componentConfig;
#Column(name = "DESCRIPTION", length = 200)
private String description;
#Column(name = "ORIGINAL_FILE_NAME",length=50)
private String originalFileName;
#Column(name = "OVERRIDE_DOCUMENT")
private Boolean overrideDocument = false;
#Column(name = "START_DATE")
private Date startDate;
#OneToMany(mappedBy="componentInfo",cascade=CascadeType.ALL,orphanRemoval=true)
private List<TransactionComponentInfo> transactionComponentInfo = new ArrayList<>(0);
#Column(name = "SHOW_GLOBAL")
private Boolean showGlobal = false;
}
Child Object
public class ReportComponentConfig {
#Id
#TableGenerator(name = "COMPONENT_CONFIG_ID", table = "ID_GENERATOR", pkColumnName = "GEN_KEY", valueColumnName = "GEN_VALUE", pkColumnValue = "COMPONENT_CONFIG_ID", allocationSize = 1, initialValue = 1)
#GeneratedValue(strategy = GenerationType.TABLE, generator = "COMPONENT_CONFIG_ID")
#Column(name = "COMPONENT_CONFIG_ID", unique = true, nullable = false)
private int id;
#Column(name = "NAME", nullable = false, length = 200)
private String name;
#Column(name = "TAG", nullable = false, length = 200)
private String tag;
#Column(name = "COMP_CONFIG", nullable = false, columnDefinition = "VARCHAR(MAX)")
private String config;
#Column(name = "PUBLISHED_CONFIG", columnDefinition = "VARCHAR(MAX)")
private String publishedConfig;
#Column(name = "IS_PUBLISHED")
private boolean published = false;
#ManyToOne
#JoinColumn(name = "COMPONENT_ID", nullable = false)
private Component component;
#ElementCollection(fetch = FetchType.EAGER)
#CollectionTable(name = "CONFIG_REPORT_MAPPING", joinColumns = #JoinColumn(name = "COMPONENT_CONFIG_ID"))
#Column(name = "REPORT_ID")
private Set<Long> reports = new HashSet<>(0);
#ElementCollection(fetch = FetchType.LAZY)
#CollectionTable(name = "CONFIG_TRANSACTION_MAPPING", joinColumns = #JoinColumn(name = "COMPONENT_CONFIG_ID"))
#Column(name = "TRANSACTION_ID")
private Set<Long> transactions = new HashSet<>(0);
#Column(name = "VIEWS", columnDefinition = "VARCHAR(MAX)")
private String views;
}
DTO
public class ComponentInfoDTO implements Cloneable {
private ReportComponentConfig componentConfig;
private String taggingKey;
private String description;
private String originalFileName;
private Date startDate;
private Boolean overrideDocument;
private Boolean showGlobal;
private ComponentInfoDTO parentComponentInfo;
public ComponentInfoDTO() {
}
public ComponentInfoDTO(ComponentInfo ci, TransactionComponentInfo transactionComponentInfo) {
this.componentConfig = ci.getComponentConfig();//this is object is null
this.taggingKey = ci.getTaggingKey();
this.description = ci.getDescription();
this.originalFileName = ci.getOriginalFileName();
this.startDate = ci.getStartDate();
this.overrideDocument = ci.getOverrideDocument();
this.showGlobal = ci.getShowGlobal();
if (transactionComponentInfo != null) {
this.parentComponentInfo = this.clone();
this.startDate = transactionComponentInfo.getStartDate();
this.overrideDocument = transactionComponentInfo.getOverrideDocument();
this.description = transactionComponentInfo.getDescription();
this.originalFileName = transactionComponentInfo.getOriginalFileName();
this.showGlobal = true;
}
}
}
New Code image
Old Code image
Edit:
Both are same but in older case i get the child object on client side and in new case i get null.
This is the data i'm getting in my older application with lazy init
[ {
"componentConfig" : {
"id" : 3,
"name" : "Monthly Origination By Region",
"tag" : "CHART_monthlyOriginationByRegion",
"config" : "xyz",
"published" : true,
"component" : {
"id" : "CHART",
"name" : "Chart",
"defaultConfig" : null,
"htmlTag" : "<chart></chart>",
"filePath" : "chart/chart.component.js",
"dependentScriptsSrc" : [ ],
"dependencies" : null
},
"reports" : [ 3 ],
"transactions" : [ 2 ],
"views" : "{\"monthlyOriginationByRegion\": {\"key\": \"MONTHLY_ORIGINATION_BY_REGION\"}}"
},
"taggingKey" : "3",
"description" : "asdfasd\nasdfadf",
"originalFileName" : "Citi Tape - 2141 - GEBL0501 - 2019 Oct 04.xlsm",
"startDate" : "2019-10-28T18:30:00.000+0000",
"overrideDocument" : true,
"showGlobal" : true,
"parentComponentInfo" : null
} ]
This is the data in new application
[ {
"componentConfig" : null,
"taggingKey" : "3",
"description" : "asdfasd\nasdfadf",
"originalFileName" : "Citi Tape - 2141 - GEBL0501 - 2019 Oct 04.xlsm",
"startDate" : "2019-10-28T18:30:00.000+0000",
"overrideDocument" : true,
"showGlobal" : true,
"parentComponentInfo" : null
} ]
Component config should not be null, if make it eager fetch it works in new app but in my older application, it is working with lazy fetch.
What you see in the debugger window is a HibernateProxy. The fields of that proxy are never initialized! The getters are intercepted and delegated to the loaded entity which is located somewhere inside the proxy.
This means you have to never call fields directly, as you don’t know if the object is a proxy or your loaded entity. You always need to work on the getters.
How are you mapping the entity to the DTO? Most likely you use field access, while you should use the getters.

Spring data JPA entity change not being persisted

I have a Spring data entity (using JPA w/ Hibernate and MySQL) defined as such:
#Entity
#Table(name = "dataset")
public class Dataset {
#Id
#GenericGenerator(name = "generator", strategy = "increment")
#GeneratedValue(generator = "generator")
private Long id;
#Column(name = "name", nullable = false)
private String name;
#Column(name = "guid", nullable = false)
private String guid;
#Column(name = "size", nullable = false)
private Long size;
#Column(name = "create_time", nullable = false)
private Date createTime;
#OneToOne(optional = false)
#JoinColumn(name = "created_by")
private User createdBy;
#Column(name = "active", nullable = false)
private boolean active;
#Column(name = "orig_source", nullable = false)
private String origSource;
#Column(name = "orig_source_type", nullable = false)
private String origSourceType;
#Column(name = "orig_source_org", nullable = false)
private String origSourceOrg;
#Column(name = "uri", nullable = false)
private String uri;
#Column(name = "mimetype", nullable = false)
private String mimetype;
#Column(name = "registration_state", nullable = false)
private int registrationState;
#OneToMany(fetch = FetchType.EAGER, cascade = {CascadeType.ALL})
#JoinColumn(name = "dataset_id")
#JsonManagedReference
private List<DatasetFile> datasetFiles;
I have the following repository for this entity:
public interface DatasetRepo extends JpaRepository<Dataset, Long> {
#Query("SELECT CASE WHEN COUNT(p) > 0 THEN 'true' ELSE 'false' END FROM Dataset p WHERE p.uri = ?1 and p.registrationState>0")
public Boolean existsByURI(String location);
#Query("SELECT a FROM Dataset a LEFT JOIN FETCH a.datasetFiles c where a.registrationState>0")
public List<Dataset> getAll(Pageable pageable);
#Query("SELECT a FROM Dataset a LEFT JOIN FETCH a.datasetFiles c WHERE a.registrationState>0")
public List<Dataset> findAll();
#Query("SELECT a FROM Dataset a LEFT JOIN FETCH a.datasetFiles c where a.guid= ?1")
public Dataset findByGuid(String guid);
}
Now - In a controller, I am fetching a dataset, updating one of its attributes and I would be expecting that attribute change to be flushed to the DB, but it never is.
#RequestMapping(value = "/storeDataset", method = RequestMethod.GET)
public #ResponseBody
WebServiceReturn storeDataset(
#RequestParam(value = "dsGUID", required = true) String datasetGUID,
#RequestParam(value = "stType", required = true) String stType) {
WebServiceReturn wsr = null;
logger.info("stType: '" + stType + "'");
if (!stType.equals("MongoDB") && !stType.equals("Hive") && !stType.equals("HDFS")) {
wsr = getFatalWebServiceReturn("Invalid Storage type '" + stType + "'");
} else if (stType.equals("MongoDB")) {
/* Here is where I'm reading entity from Repository */
Dataset dataset = datasetRepo.findByGuid(datasetGUID);
if (dataset != null) {
MongoLoader mongoLoader = new MongoLoader();
boolean success = mongoLoader.loadMongoDB(dataset);
logger.info("Success: " + success);
if (success) {
/* Here is where I update entity attribute value, this is never flushed to DB */
dataset.setRegistrationState(1);
}
wsr = getWebServiceReturn(success ? 0 : -1, "Successfully loaded dataset files into " + stType + " storage", "Failed to load dataset files into " + stType + " storage");
}
}
return wsr;
}
Thank you
You need to annotate the method of request mapping with #Transactional.
Why? If you want to modify an object in memory and then it is updated transparently in the database you need do it inside an active transaction.
Don't forget you're using JPA (spring-data is using JPA) and if you want your Entity will be in a managed state you need an active transaction.
See:
http://www.objectdb.com/java/jpa/persistence/update
Transparent Update Once an entity object is retrieved from the
database (no matter which way) it can simply be modified in memory
from inside an active transaction:
Employee employee = em.find(Employee.class, 1);
em.getTransaction().begin();
employee.setNickname("Joe the Plumber");
em.getTransaction().commit();

Resources