Entity manager changes detached entities - spring

Help me.
Why does the collection in the entity change after the transaction?
My entity:
#Entity
class Entity(
#Id
val uuid: UUID,
#OneToMany(cascade = [CascadeType.PERSIST, CascadeType.REMOVE])
#JoinTable(
name = "entities_items",
joinColumns = [JoinColumn(name = "entity_uuid")],
inverseJoinColumns = [JoinColumn(name = "item_uuid")]
)
val items: MutableList<Item>
)
My test:
#SpringBootTest
internal class EntityTest {
#Autowired
lateinit var entityRepository: EntityRepository
#Autowired
lateinit var transactionManager: PlatformTransactionManager
#Test
fun will_added_item() {
val entityBefore = entityRepository.findById(entityId).get()
// entityBefore.items.size == 0
TransactionTemplate(transactionManager).execute { _ ->
val entity = entityRepository.findById(entityId).get()
entity.items.add(item)
}
// entityBefore.items.size == 1 <-- ???
val entityAfter = entityRepository.findById(entityId).get()
}
}
Interestingly, if I add any call to the collection before the transaction, everything will be fine.
Spring Boot + Hibernate + JUnit

please ignore this wrong answer - can't seem to delete it on my phone!
This is because when you retrieve an item by id within a transaction multiple times, Hibernate will give you same object as it is managing the objects involved in the transaction as a unit of work.
Hence entityBefore and entity are pointing at the same object.
Ah sorry, ignore that - I see first retrieval was outside the tx!

Related

Hibernate search does not remove old value from lucene index when the object is deleted via an #NoRepositoryBean Jpa method

I have a NoRepositoryBean Jpa interface that has one custom jpa method called deleteAllByIdIn(...) which is inherited by some concrete JpaRepositories. For some reason this custom delete method is ignored by Hibernate Search. Whenever an entity is deleted through this custom method its value is not removed from the lucene index after the delete is done. I will explain the problem some more further down this post; but first here's the code
#NoRepositoryBean
public interface NameTranslationDao<T extends NameTranslation> extends JpaRepository<T, Long> {
#Modifying
#Transactional
#Query(value = "DELETE FROM #{#entityName} c WHERE c.id IN :translationsToDelete")
public void deleteAllByIdIn(#Param("translationsToDelete") Set<Long> translationsToDelete);
}
Heres a JpaRepository subclass that extends this interface:
#Repository
#Transactional(readOnly = true)
public interface LifeStageCommonNameTranslationDao extends CommonNameTranslationDao<LifeStageCommonNameTranslation> {
}
Theres another #NoRepositoryBean interface in-between the concrete JpaRepository and the NameTranslationDao NoRepositoryBean. That one is called CommonNameTranslationDao but it doesn't override the custom method in any way, so it is unlikely the cause of the problem, nevertheless heres the code of that repository:
#NoRepositoryBean
public interface CommonNameTranslationDao<T extends NameTranslation> extends NameTranslationDao<T> {
#Deprecated
#Transactional(readOnly = true)
#Query("SELECT new DTOs.AutoCompleteSuggestion(u.parent.id, u.autoCompleteSuggestion) FROM #{#entityName} u WHERE u.autoCompleteSuggestion LIKE :searchString% AND deleted = false AND (u.language.id = :preferredLanguage OR u.language.id = :defaultLanguage)")
List<AutoCompleteSuggestion> findAllBySearchStringAndDeletedIsFalse(#Param("searchString") String searchString, #Param("preferredLanguage") Long preferredLanguage, #Param("defaultLanguage") Long defaultLanguage);
#Transactional(readOnly = true)
#Query(nativeQuery = true, value = "SELECT s.translatedName FROM #{#entityName} s WHERE s.language_id = :preferredLanguage AND s.parent_id = :parentId LIMIT 1")
public String findTranslatedNameByParentAndLanguage(#Param("preferredLanguage") Long languageId, #Param("parentId") Long parentId);
#Modifying
#Transactional
#Query(nativeQuery = true, value = "DELETE FROM #{#entityName} WHERE id = :id")
void hardDeleteById(#Param("id") Long id);
#Modifying
#Transactional
#Query(nativeQuery = true, value = "UPDATE #{#entityName} c SET c.deleted = TRUE WHERE c.id = :id")
void softDeleteById(#Param("id") Long id);
}
Also, heres the code of the LifeStageCommonNameTranslation entity class:
#Entity
#Indexed
#Table(
uniqueConstraints = {
#UniqueConstraint(name = "UC_life_cycle_type_language_id_translatedName", columnNames = {"translatedName", "parent_id", "language_id"})
},
indexes = {
#Index(name = "IDX_lifestage", columnList = "parent_id"),
#Index(name = "IDX_translator", columnList = "user_id"),
#Index(name = "IDX_species_language", columnList = "language_id, parent_id, deleted"),
#Index(name = "IDX_autoCompleteSuggestion_language", columnList = "autoCompleteSuggestion, language_id, deleted")})
public class LifeStageCommonNameTranslation extends NameTranslation<LifeStage> implements AuthorizationSubject {
#Id #DocumentId
#GenericGenerator(
name = "sequenceGeneratorLifeStageCommonNameTranslation",
strategy = "org.hibernate.id.enhanced.SequenceStyleGenerator",
parameters = {
#org.hibernate.annotations.Parameter(name = "sequence_name", value = "_lifestagecommonnametranslation_hibernate_sequence"),
#org.hibernate.annotations.Parameter(name = "optimizer", value = "pooled"),
#org.hibernate.annotations.Parameter(name = "initial_value", value = "1"),
#org.hibernate.annotations.Parameter(name = "increment_size", value = "25"),
#org.hibernate.annotations.Parameter(name = "prefer_sequence_per_entity", value = "true")
}
)
#GeneratedValue(
strategy = GenerationType.SEQUENCE,
generator = "sequenceGeneratorLifeStageCommonNameTranslation"
)
#Field(analyze = Analyze.NO, store = Store.YES, name = "parentId")
private Long id;
#IndexedEmbedded(includeEmbeddedObjectId = true)
#ManyToOne(fetch = FetchType.LAZY)
private LifeStage parent;
#Field(index = NO, store = Store.YES)
private String autoCompleteSuggestion;
//Getters and setters ommitted
The problem is the following: Whenever i use the inherited deleteAllByIdIn() method on LifeStageCommonNameTranslationDao then Hibernate Search will not remove the autoCompleteSuggestion field value from the lucene index after the entity has been deleted. If however i use the standard deleteById() JpaRepository method to delete the entity then the field value is removed from the lucene index.
Both the custom and the standard delete method were called within a #Transactional annotated method and i also called the flush() jpaRepository method right afterwards. I did this because I've read that this can sometimes help to update the lucene index. But in the case of deleteAllByIdIn() calling flush() afterwards did not help at all.
I already ruled out the possiblity that the problem was caused by the spEL expression in the SQL query. I tested this by replacing #{#entityName} with a concrete entity name like LifeStageCommonTranslation and then calling the deleteAllByIdIn() delete method. But the problem still persisted. The lucene index still did not remove the autoSuggestionText field value after the delete.
I can easily solve this problem by simply using the standard jpa method deleteById() but i want to know why the custom made jpa method deleteAllByIdIn() does not cause Hibernate search to update the lucene index.
Hibernate Search detects entity change events happening in your Hibernate ORM Session/EntityManager. This excludes insert/update/delete statements that you wrote yourself in JPQL or native SQL queries.
The limitation is documented here: https://docs.jboss.org/hibernate/stable/search/reference/en-US/html_single/#limitations-changes-in-session
The workaround is documented there too:
One workaround is to reindex explicitly after you run JPQL/SQL queries, either using the MassIndexer or manually.
EDIT: And of course your workaround might be valid as well, if deleteById loads the entity in the session before deleting it (I'm not that familiar with the internals of Spring Data JPA):
I can easily solve this problem by simply using the standard jpa method deleteById() but i want to know why the custom made jpa method deleteAllByIdIn() does not cause Hibernate search to update the lucene index.

Hibernate detached entity passed to persist error in Spring Boot in Kotlin using OneToOne with MapsId

I have the following entities in a fairly simple and straightforward Spring Boot application in Kotlin:
#Entity
class Target(
#Id #GeneratedValue var id: Long? = null,
// ... other stuff
)
#Entity
class Ruleset(
#OneToOne(fetch = FetchType.LAZY) #MapsId
var target: Target,
#Id #GeneratedValue var id: Long? = null,
// ... other stuff
)
And I have the following code to create them upon startup of a #Component:
#PostConstruct
#Transactional
fun init() {
val target = Target()
targetRepository.save(target)
val rule = Ruleset(target)
rulesetRepository.save(rule)
}
And when this runs I get the "detached entity passed to persist: com.mystuff.Target" error. I've used this approach in the past (see here: https://vladmihalcea.com/the-best-way-to-map-a-onetoone-relationship-with-jpa-and-hibernate/) without issue, although never in trying to create them at the same time in the same method. I've also tried using the entity passed back by the .save() call on the Target repository in the persist of the Ruleset object with no success.
I am able to fix this if I go back to the "normal" way of doing a OneToOne relationship:
#Entity
class Target(
#OneToOne(mappedBy = "target", cascade = [CascadeType.ALL],
fetch = FetchType.LAZY, optional = false)
var ruleset: Ruleset?
#Id #GeneratedValue var id: Long? = null
)
#Entity
class Ruleset(
#OneToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "target_id")
var target: Target,
#Id #GeneratedValue var id: Long? = null,
)
But this is annoying as it forces me to pass a null into the Target constructor and then update it immediately after creating the Ruleset. I can't figure out why the other, simpler approach doesn't work.

"could not initialize proxy - no Session" For Multiple ManyToMany relationships in the parent

I have a Parent User Class that has multiple ManyToMany Relationships.
#Table(name = "user")
public class User {
..
#ManyToMany(fetch = FetchType.LAZY, cascade = {CascadeType.MERGE, CascadeType.DETACH})
#JoinTable(
name = "user_address",
joinColumns = { #JoinColumn(name = "user_id")},
inverseJoinColumns = { #JoinColumn(name = "address_id")}
)
#JsonIgnore
private final List<Address> addresses = new ArrayList<Address>();
#ManyToMany(fetch = FetchType.LAZY, cascade = {CascadeType.MERGE, CascadeType.DETACH})
#JoinTable(
name = "reports",
joinColumns = { #JoinColumn(name = "user_id")},
inverseJoinColumns = { #JoinColumn(name = "reports_id")}
)
#JsonIgnore
private final List<Reports> reports = new ArrayList<Reports>();
}
When I access the FIRST ManyToMany property, everything works fine. However, immediately after
accessing the first, when I try to access the SECOND ManyToMany Property I get the "could not initialize proxy - no Session" exception:
#Component
public class Combiner {
public void combineData() {
...
List<Address> addresses = user.getAddress(); // This works
List<Reports> reports = user.getReports(); // Get the error here
..
}
}
The Address and Reports classes have the inverse relationship as many ManyToMany back to the User Entity Above.
public class Address {
#ManyToMany(mappedBy = "addresses", fetch = FetchType.LAZY)
private final List<User> users = new ArrayList<User>();
}
public class Reports {
#ManyToMany(mappedBy = "reports", fetch = FetchType.LAZY)
private final List<User> users = new ArrayList<User>();
}
I tried searching SO for the same error where there are MULTIPLE relationships like mine and the first passes but second fails, but could'nt find a post (or google couldn't understand the search terms, if anyone knows a pre-existing one - please let me know).
Could someone assess what else Im missing?
I've tried these so far to no avail:
Added #Transactional to the parent Service class that calls Combiner above
Made the second failing relationship EAGER. (as i understand it you cant make BOTH EAGER since i get a multiple bags error probably because of Cartesian join)
AM Using SpringBoot (2.2.4) with Hibernate Core {5.4.10.Final}
Approach one:
Make #ManyToMany uni-directional. The exception clearly says it can not initialize the collection of role you have in User class.
As you asked in the comment section Why can't this use case be Bi Directional - You can make this bi-directional as well.
Approach two: make collection of role EAGER or use Hibernate.initialize() to initialize the collection.
Bonus: you can make both collection EAGER by using Set not List.

How to Lazy Load object from Lists in Java JPA?

I am having these Entities: DocumentType, UserGroup, User
DocumentType.java has #ManyToMany Set of UserGroup:
#ManyToMany(cascade = {CascadeType.PERSIST, CascadeType.MERGE})
#JoinTable(name = "review_type", joinColumns = #JoinColumn(name="doc_type"),
inverseJoinColumns = #JoinColumn(name="user_group_id") )
private Set<UserGroup> reviewUserGroups;
UserGroup.java has #ManyToMany Set of User:
#ManyToMany
#JoinTable(name = "group_users", joinColumns = #JoinColumn(name = "group_id"),
inverseJoinColumns = #JoinColumn(name = "user_id"))
private Set<User> users;
What I want to do implement this code:
#Transactional
private void createDocuments(int avgDocsPerUser) {
List<DocumentType> documentTypes = documentTypeRepository.findAll();
int documentTypesCount = documentTypes.size();
List<User> users = userRepository.findAll().stream().filter(user -> !user.isAdmin()).collect(Collectors.toList());
int usersCount = users.size();
int documentsToCreate = (int) Math.floor(Math.random() * (usersCount * avgDocsPerUser)) + 1;
List<Document> documentList = new ArrayList<>();
while (documentList.size() < documentsToCreate) {
DocumentType documentType = documentTypes.get((int) Math.floor(Math.random() * documentTypesCount));
User user = documentType
.getSubmissionUserGroups()
.stream().findAny()
.get().getUsers()
.stream().findAny().get();
// create new document here and add User info to it
}
documentRepository.saveAll(documentList);
}
The problem that I keep getting error:
Caused by: org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: it.akademija.wizards.entities.DocumentType.submissionUserGroups, could not initialize proxy - no Session
I want to avoid EAGER fetching. How to implement this code so I can randomly get User that is a part of UserGroup which is a part of SubmissionUserGroups in DocumentType object.
Part of your problem is likely that you've used the #Transactional annotation on a private method. According to the docs, this doesn't work:
When using proxies, you should apply the #Transactional annotation only to methods with public visibility. If you do annotate protected, private or package-visible methods with the #Transactional annotation, no error is raised, but the annotated method does not exhibit the configured transactional settings. Consider the use of AspectJ (see below) if you need to annotate non-public methods.
In addition, I find the way you get a User from the document type a bit hard to understand. Part of the issue there is the number of time you stream through collections, find something, and then stream through another collection.
It might be easier (and more in line with Spring idioms) to inject the UserRepository into this class and do a separate query here. If this method is also public, I believe it would be included in the same transaction so you wouldn't suffer the performance overhead of having to open another session.
However, you should do some more research on this. You might find this other post helpful: How to load lazy fetched items from Hibernate/JPA in my controller.

What is the most convenient way to deal with nested objects in Room?

I want to save the server’s response in database (class Parent). The json has nested object, which also should be saved in database in new table (class Nested). The problem is what I don’t know how to write class Parent and ParentDao to make it use NestedDao
#Entity
data class Parent(
#PrimaryKey(autoGenerate = true)
var id: Long? = null,
#SerializedName(«nested»)
val homeTeam: Nested,
//other fields
)
#Entity
data class Nested(
#PrimaryKey(autoGenerate = true)
var nestedId: Long? = null,
#SerializedName("name")
val name: String,
//other fields
)
#Dao
interface ParentDao {
#Query("SELECT * FROM parent»)
fun getData(): Single<List<Parent>>
#Insert
fun insert(matches: List<Parent>)
}
This gives me an error: Cannot figure out how to save this field into database. You can consider adding a type converter for it.
So, what should I do to save and query Parent with Nested at once?
I don't know if you've succeed or not, but here is my answer I hope it'll help you.
That's what I used for my project and what is recommended for Room in Android docs
#Entity
data class Parent(
#PrimaryKey(autoGenerate = true)
var id: Long? = null,
#Embedded #ColumnInfo(name= "nested")
val homeTeam: Nested,
//other fields
)
data class Nested(
#PrimaryKey(autoGenerate = true)
var nestedId: Long? = null,
#ColumnInfo(name= "name")
val name: String,
//other fields
)
#Dao
interface ParentDao {
#Query("SELECT * FROM parent»)
fun getData(): Single<List<Parent>>
#Insert
fun insert(matches: List<Parent>)
}

Resources