findAllBy JPA with embeddedId - kotlin

I have this Document hibernate jpa entity with an EmbeddedId:
#Entity
data class Document(
#EmbeddedId
#NotNull
val documentExpertId: DocumentExpertId,
// other fields
)
#Embeddable
data class DocumentExpertId(
#Column(nullable = false)
val expertId: String,
#Column(nullable = false)
val name: String
) : Serializable
To get all the documents by expertId, I would like to have my document JPA repository interface method called:
fun findAllByExpertId(String expertId): List<Document>
But, the only way I found to do so is:
fun findAllByDocumentExpertIdExpertId(String expertId): List<Document>
Is there an other way to have a better name for this method?

You could change the ID and column definition to:
#EmbeddedId
#NotNull
val documentExpertKey: DocumentExpertKey,
#Column(name = "expertId", nullable = false)
val id: String,
So that your query could be:
fun findAllByDocumentExpertKeyId(String expertId): List<Document>
That looks a little more normal to me.

Related

assign "val" for unit test in kotlin

I am writing unit tests in kotlin, for this purpose I need to assign value on a "val", here is the simplified version of the code:
#Entity
#Table(name = "Request")
data class Request(
#Column(name = "Name")
val name: String,
) {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "id")
var id: Long? = null
#CreationTimestamp
#Column(name = "Created")
val created: LocalDateTime = LocalDateTime.now()
}
#Test
fun `test one`() {
val name = RandomStringUtils.randomNumeric(10)
val id = Random.nextLong(100)
val created = LocalDateTime.now().minusHours(48)
val request = Request(playerUid = playerUid).apply {
id = id
created = created
}
}
it has an compile error when assigning "created" in the test. How should I manage this unit test since I need to set my desire "created" value? (I can not touch any part of the "Request class")
If you cannot change any part of the Request class then you will not be able to change created.
You will either need to test created by using an approximate test range (created needs to be 0<now<2s sort of thing)
It is a design flaw to encode a static accessor to functions like LocalDateTime.now() - this should be set externally in a service class. If you really cannot do that, then here is another hacky approach:
add a CLOCK object somewhere (does not need to be in a companion object) but ultimately you have to change the created assignment:
#Entity
#Table(name = "Request")
data class Request(
#Column(name = "Name")
val name: String,
) {
companion object {
/** used for carrying a Clock only in the case of tests **/
val CLOCK = ThreadLocal<Clock>()
}
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "id")
var id: Long? = null
#CreationTimestamp
#Column(name = "Created")
val created: LocalDateTime = LocalDateTime.now(CLOCK.get() ?: Clock.systemUTC())
}
Normally you don't touch that CLOCK but in the Unit Tests you define a
private val fixedClock = Clock.fixed(Instant.parse("2022-08-29T08:20:50Z"), ZoneOffset.UTC)
then you need
#BeforeEach
fun beforeEach() {
// this is necessary because the serialization of MemberMentorCommon.weeksOnPlan uses a clock
CLOCK.getOrSet { fixedClock }
}
#AfterEach
fun afterEach() {
CLOCK.remove()
}
Yes, ThreadLocals are nasty, but this allows you to change the behaviour of the Request class to override the now() function

org.springframework.data.mapping.MappingException with Kotlin and sealed classes

I am having issues with spring data (elasticsearch) and Kotlin sealed classes. It seems it's detecting two id mappings. The classes:
#JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "#type")
#JsonSubTypes(
JsonSubTypes.Type(value = AbstractSectionV1Dto.HtmlSectionV1Dto::class, name = "HTML"))
sealed class AbstractSectionV1Dto(
open val number: Int?,
open val id: Long?,
open val questionAnswers: List<AbstractQuestionAnswerV1Dto>,
open val rightQuestionAnswers: Int?,
open val questionAnswersSize: Int?) : Dto {
data class HtmlSectionV1Dto(
override val number: Int?,
override val id: Long,
override val questionAnswers: List<AbstractQuestionAnswerV1Dto>,
override val rightQuestionAnswers: Int?,
override val questionAnswersSize: Int?,
val html: String?) : AbstractSectionV1Dto(number, id, questionAnswers, rightQuestionAnswers, questionAnswersSize)
(...)
}
when I save the document to elasticsearch, I get
org.springframework.data.mapping.MappingException: Attempt to add id property private final java.lang.Long com.package.AbstractSectionV1Dto.id but already have property private final long com.package.AbstractSectionV1Dto$TextSectionV1Dto.id registered as id. Check your mapping configuration!
I also tried setting #Id only on the top class and on both. Any clues?
I think I got it working by placing org.springframework.data.annotation.Transient in the sealed class id, but it would be nice if someone could confirm or say something about the best solution to this problem.

Insert in to join table Spring JPA with Kotlin

My entiti of member:
#Entity(name = "Members")
data class Member(
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Long? = null,
#Column(unique = true)
#Basic
val uid: String,
#Column(unique = true)
#Basic val email: String,
#ManyToMany(fetch = FetchType.LAZY, cascade = [CascadeType.ALL])
#JoinTable(name = "groups_members")
#JsonIgnore
val groups: Collection<Group?> = ArrayList(),
#OneToMany(mappedBy = "member")
#JsonIgnore
val tasks: List<Task?> = ArrayList(),
#OneToMany(mappedBy = "creator")
#JsonIgnore
val createdTasks: List<Task?> = ArrayList(),
#OneToMany(mappedBy = "creator")
#JsonIgnore
val createdMeetings: List<Meeting?> = ArrayList(),
#OneToMany(mappedBy = "creator")
#JsonIgnore
val createdGroups: List<Group?> = ArrayList(),
#ManyToMany(fetch = FetchType.LAZY, cascade = [CascadeType.ALL])
#JoinTable(name = "meetings_members")
#JsonIgnore
val meetings: List<Meeting?> = ArrayList()
)
entity of group of members:
package com.kotlincoders.tmsapi.data
import com.fasterxml.jackson.annotation.JsonIgnore
import javax.persistence.*
#Entity(name = "team")
data class Group(
#Id #GeneratedValue(strategy = GenerationType.IDENTITY) val id: Long? = null,
#Column(unique = true, name = "name")
#Basic
val name: String,
#Basic
val description: String,
#ManyToMany(fetch = FetchType.LAZY, cascade = [CascadeType.ALL], mappedBy = "groups")
#JsonIgnore
val members: List<Member> = ArrayList(),
#OneToMany(mappedBy = "group")
#JsonIgnore
val tasks: List<Task> = ArrayList(),
#JsonIgnore
#ManyToOne val creator: Member
){
override fun toString(): String {
return "Group(id=${this.id},name=${this.name},description=${this.description})"
}
fun addMember(member: Member){
members.plus(member)
member.groups.plus(this)
}
}
Method in controller:
#PostMapping("/add/")
fun add(#RequestBody request: AddGroupRequest) {
val member = memberService.findByUid(request.creatorUid)
var group = Group(name = request.title, creator = member, description = request.description)
group.addMember(member)
groupService.save(group)
}
But when i try add member to group i don't have any exception but nothing is inserted to join table and i don know why.
Groups are added succesfully.
I have the same problem with others entities, and i tryed many ways to solve it and nothing works.
The problem here is related to lazy fetching, to solve that try to use kotlin allOpen plugin (More info in the link at the end of the answer).
Using data classes (with val) with JPA is not the best option, read this quote from spring example:
Here we don’t use data classes with val properties because JPA is not designed to work with immutable classes or the methods generated automatically by data classes. If you are using other Spring Data flavor, most of them are designed to support such constructs so you should use classes like data class User(val login: String, …​) when using Spring Data MongoDB, Spring Data JDBC, etc.
https://github.com/spring-guides/tut-spring-boot-kotlin#persistence-with-jpa

Room, Boolean and kotlin interface lead to ambiguous getter error

Having following interface and class:
interface IUserCreated {
val userCreated: Boolean
}
#Entity
data class Test(
#PrimaryKey #ColumnInfo(name = "test_id") var id: Long,
#ColumnInfo(name = "user_created") override val userCreated: Boolean
) : IUserCreated
This leads to following error:
error: Ambiguous getter for Field(element=userCreated, name=userCreated, type=boolean, affinity=INTEGER, collate=null, columnName=user_created, defaultValue=null, parent=null, indexed=false, nonNull=true).
All of the following match: getUserCreated, isUserCreated. You can #Ignore the ones that you don't want to match.
Question
How can I use the #Ignore annotation in this example? The userCreated boolean field is part of the entity, I just want to ignore the unused getUserCreated and tell room that it should use the default kotlin getter function isUserCreated
Note
I know I can solve this by doing following:
interface IUserCreated {
val internalUserCreated: Boolean
fun getUserCreated(): Boolean = internalUserCreated
}
#Entity
data class Test(
#PrimaryKey #ColumnInfo(name = "test_id") var id: Long,
#ColumnInfo(name = "user_created") override val internalUserCreated: Boolean
) : IUserCreated
I wonder if I can somehow solve this without the workaround, I don't want to do this for every boolean field in any entity I use, but I don't know how I can use the #Ignore annotation to fix the problem.

Cannot save data model that contains List<Model> with Room ORM Kotlin

I have a problem with Room ORM working on Kotlin. My task is having ability to save and get data models RouteTemplateModel, that contains list of addresses of type AddressModel and object of class RouteModel that contains title of the specific route. Here is my code:
AddressModel.kt
#Entity(foreignKeys = arrayOf(
ForeignKey(entity = RouteModel::class,
parentColumns = arrayOf("routeId"),
childColumns = arrayOf("parentId"))))
data class AddressModel(
#PrimaryKey(autoGenerate = true)
var addressId: Long,
var parentId: Long,
var street: String,
var house: String,
var entrance: String,
var title: String){
constructor(): this(0, 0, "", "", "", "")
}
RouteModel.kt
#Entity
data class RouteModel(
#PrimaryKey(autoGenerate = true)
var routeId: Long,
var title: String) {
constructor() : this(0, "")
}
Here is my simple models, I found in documentation of Room that for creating relations between models I need to use #ForeignKey and #Relation
So with code samples in doc and tutorials I create RouteTemplateModel that contains object of RouteModel and list of AddressModels. Here is the class
RouteTemplateModel
class RouteTemplateModel{
private var id: Long = 0
#Embedded
private var routeModel: RouteModel = RouteModel()
#Relation(parentColumn = "routeId", entityColumn = "parentId")
private var addressList: List<AddressModel> = listOf()
constructor()
constructor(id: Long, routeModel: RouteModel, title: String,
addressList: List<AddressModel>){
this.id = id
this.routeModel = routeModel
this.addressList = addressList
}
fun getId(): Long{
return id
}
fun getRouteModel(): RouteModel{
return routeModel
}
fun getAddressList(): List<AddressModel>{
return addressList
}
fun setId(id: Long){
this.id = id
}
fun setRouteModel(routeModel: RouteModel){
this.routeModel = routeModel
}
fun setAddressList(addressList: List<AddressModel>){
this.addressList = addressList
}
}
So what`s a problem? I am getting such errors:
Error:The columns returned by the query does not have the fields [id]
in com.innotech.webcab3kotlin.model.RouteTemplateModel even though
they are annotated as non-null or primitive. Columns returned by the
query: [routeId,title]
And
Error:Type of the parameter must be a class annotated with #Entity or
a collection/array of it.
It is a real problem, because if my trying to fix first error and annotate in RouteTemplateModel id variable to return this column too, I need annotate class as Entity (like in second error), but when I do it I am getting an error
Error:Entities cannot have relations.
Here is AppDatabase.kt
#Database(entities = arrayOf(RouteModel::class, AddressModel::class), version = 1)
abstract class AppDatabase : RoomDatabase() {
abstract fun getRouteDao(): RouteDao
}
and RouteDao.kt
#Dao
interface RouteDao {
#Query("SELECT routeId, title FROM RouteModel")
fun getAll(): List<RouteTemplateModel>
#Insert
fun insertAll(vararg models: RouteTemplateModel)
#Delete
fun delete(model: RouteTemplateModel)
}
Thats really confusing. Please, help me)
Your "parentId" column is capable of holding long value only, make its type to "Text" then create a TypeConverter from "List" to String and vice a versa for reference please have a look at link .