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

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.

Related

Spring Data JDBC Kotlin NoSuchMethod error: Dialect.getIdGeneration()

I'm writing a very simple Spring Data JDBC repository in Kotlin (using Postgres as the database):
data class Label(
#Id
#GeneratedValue
#Column( columnDefinition = "uuid", updatable = false )
val id: UUID,
val name: String
)
#Repository
interface LabelRepository: CrudRepository<Label, UUID> {}
When I do repository save:
val l = Label(id = UUID.randomUUID(), name = "name")
labelRepo.save(l)
It works fine. But since id is not null Spring Data JDBC will always treat it as an "update" to an existing label entity instead of creating a new one with generated ID.
So I changed id: UUID to id: UUID? And having val l = Label(id = null, name = "name")
But call the same save() method gives me:
java.lang.NoSuchMethodError: 'org.springframework.data.relational.core.dialect.IdGeneration org.springframework.data.relational.core.dialect.Dialect.getIdGeneration()'
I have tried a solution here: https://jivimberg.io/blog/2018/11/05/using-uuid-on-spring-data-jpa-entities/
But it didn't work, still gives me the same error
Wondering what's the cause of this and why this error pops up only when I change UUID to UUID??
nvm, turns out I have to use the implementation("org.springframework.boot:spring-boot-starter-data-jdbc") dependency instead of implementation("org.springframework.data:spring-boot-starter-data-jdbc:2.1.3")

Entity manager changes detached entities

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!

How to link an entity from one table into another?

How can I link an entity that already exists in another table into my important_table? I could insert the ID but then that would require a query. What I want is that the system automatically maps the element in the people_table to the important_table.
#Entity(name = "important_table")
data class ImportantEntity(
#Id #GeneratedValue #Column(name = "id")
val id: Short = 0,
#Embedded
val person: Person
)
Person Entity
#Entity(name = "person_table")
data class PersonEntity(
#Id #GeneratedValue #Column(name = "id")
val id: Long = 0,
...
)
I tried embedded but that creates a duplicate Person in the db. I want the link so that I can find "important" people easy and still get the same data.
Use a #OneToOne mapping to let Hibernate (or any ORM) knows you are linking the tables together.
#Entity(name = "important_table")
data class ImportantEntity(
#Id #GeneratedValue #Column(name = "id")
val id: Short = 0,
#OneToOne(fetch = FetchType.EAGER, cascade = CascadeType.ALL)
val person: Person
)
When you say: I could insert the ID but then that would require a query, indeed, it does... But with the FetchType.EAGER, it's your ORM that will execute the extra query for you... It is transparent.
Nonetheless, be careful with that, if you don't pay attention to your relationships, you can end up loading the entire db in memory...
As explained in this site, and this tuto, #Embedded is used to help having a nice and clean object definition, while storing the data into one table instead of 2... For instance, in your case, you would be storing the data from the class Person into the table ImportantTable.
Hope it helps !

Is #ManyToOne's "optional" param automatically set using Kotlin's nullability

I read that specifying optional = false in the #ManyToOne association annotation could help Spring improve the performance of the queries.
In a Kotlin data class entity, do I actually need to specify the parameter in the annotation, or can Spring figure this out by itself using the nullability of the item field?
For instance, if I have the following declaration:
#Entity
#Table(name = ACCESS_LOGS_ARCHIVES_TABLE, indexes = [
Index(name = "access_logs_archives_item_idx", columnList = "access_item_id")
])
data class AccessLogArchive(
val date: LocalDate,
#ManyToOne(optional = false)
#JoinColumn(name = "access_item_id", nullable = false)
val item: AccessLogItem,
val occurrences: Int
) {
#Id
#GeneratedValue
var id: Long? = null
}
#Entity
#Table(name = ACCESS_ITEMS_TABLE)
data class AccessLogItem(
#Column(length = 3) val code: String,
#Column(columnDefinition = "text") val path: String,
#Column(length = 10) val verb: String
) {
#Id
#GeneratedValue
var id: Long? = null
}
In this case, I would for instance expect Spring to know that the item field is not nullable, and thus the relationship should be understood as optional=false even without specifying it as I did. Is this the case?
Same question goes for the #JoinColumn's nullable = false, by the way.
Consider a simple entity like a Room which has a #ManyToOne relationship to House.
#Entity
class Room(
#ManyToOne(optional = true)
val house: House
) {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
val id: Long = 0
}
JPA will create a room table with a column
`house_id` bigint(20) DEFAULT NULL
If you specify #ManyToOne(optional = false)
the column will look like this:
`house_id` bigint(20) NOT NULL
By specifiying optional you tell JPA how the schema should be generated, whether the column can be NULL or not.
At runtime trying to load a Room without a House will cause an Exception if the house property is not nullable (House instead of House?) even when value of optional is true.
The same applies to #JoinColumn.
Is #ManyToOne's “optional” param automatically set using Kotlin's
nullability?
No it is not. It is independent from that and by default set to true.
Conclusion: In order for you schema to reflect your entities it is a good idea to use optional = true if the house property would be nullable and optional = false if the house property would be non-nullable.

Hibernate Sequence Conflict after using Audited annotation

I'm using hibernate in my spring boot application
my domain model is like this
#Entity
#Table(name = "skill")
#Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE)
#Document(indexName = "skill")
#Audited
public class Skill implements Serializable {
private static final long serialVersionUID = 1L;
#Id
#GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "sequenceGenerator")
#SequenceGenerator(name = "sequenceGenerator")
private Long id;
}
The increment size of sequence is 50 and is working properly
but when I add Envers Audited annotation I see this error
conflicting values for 'increment size'. Found [50] and [1]
How can I resolve this conflict?
This doesn't sound like an Envers problem but a general mapping problem.
When you add an #Audited annotation, that simply informs Envers that it should inspect that particular entity mapping during Hibernate bootstrap and create the necessary audit objects to store the entity state during each transaction.
The generated Envers objects use their own sequence generators and primary key. The user defined generation strategy, sequences, etc are all ignored in the Envers object because the associated column is meant to just be a copy/pass-thru value, nothing special.
In other words, the Envers table would have a PK defined that mirrors this POJO:
#Embeddable
public class EnversSkillId implements Serializable {
#Column(name = "REV", nullable = false, updatable = false)
private Integer rev;
#Column(name = "id", nullable = false, updatable = false)
private Long id;
}
When Envers generates the audit record, it automatically uses its internal sequence generator to get the next value and assign it to EnversSkillId#rev and copies your entity's id value directly into the EnversSkillId#id property.
So as mentioned in the comments, your problem is very unlikely related to Envers.

Resources