I have two entities: Book and Page. Book can have many pages.
#Entity(name = "book")
#RegisterForReflection
internal data class Book (
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "id")
val bookId: Long? = null,
#OneToMany(mappedBy = "book", fetch = FetchType.LAZY)
#Fetch(FetchMode.JOIN)
val pages: MutableSet<Page> = mutableSetOf(),
)
#Entity(name = "page")
#RegisterForReflection
internal data class Page(
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "id")
val pageId: Long? = null,
#ManyToOne(fetch = FetchType.LAZY)
#Fetch(FetchMode.JOIN)
#JsonIgnore
var book: Book? = Book(),
)
Both entities are persisted and created in database fine. But if I try to fetch Book, I get LazyInitializationException. Although I've tried fetching it with Mutiny.fetch or using LEFT JOIN FETCH it always throws the same error. Below the way I persist/fetch:
#ApplicationScoped
internal class BookRepo : PanacheRepository<Book> {
fun getBook(bookId: Long): Uni<Book> {
return findById(bookId)
.call { it -> Mutiny.fetch(it.pages) }
.map {
it ?: throw RuntimeException("Not Found")
}
}
fun persistBook(book: Book): Uni<Book> {
return Panache.withTransaction {
persist(book)
}
}
}
#ApplicationScoped
internal class PageRepo : PanacheRepository<Page> {
fun persistPage(page: Page, bookId: Long): Uni<Page> {
return Panache.withTransaction {
getSession().chain { s ->
val book = s.getReference(Book::class.java, bookId)
page.book = book
persist(page)
}
}
}
}
What am I doing wrong? I've created a repo with this small example, there are sequence of CURL requests to reproduce the error (please check the readme file).
The problem is that the default equals and hashcode for the entities include the associations.
So, when you run the first select query, if they are called before you've fetched the association, it will cause the error.
You need to change your entities to something like:
#Entity(name = "page")
internal data class Page(
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "id")
val pageId: Long? = null,
#ManyToOne(fetch = FetchType.LAZY)
#Fetch(FetchMode.JOIN)
#JsonIgnore
var book: Book? = null,
){
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as Page
if (pageId != other.pageId) return false
return true
}
override fun hashCode(): Int {
return pageId?.hashCode() ?: 0
}
}
package org.acme
import org.hibernate.annotations.Fetch
import org.hibernate.annotations.FetchMode
import javax.persistence.*
#Entity(name = "book")
internal data class Book (
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "id")
val bookId: Long? = null,
#OneToMany(mappedBy = "book", fetch = FetchType.LAZY)
#Fetch(FetchMode.JOIN)
var pages: MutableSet<Page> = mutableSetOf(),
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as Book
if (bookId != other.bookId) return false
return true
}
override fun hashCode(): Int {
return bookId?.hashCode() ?: 0
}
}
Normally, you shouldn't include the identifier, but there are no other fields in this simple example.
The Hibernate ORM documentation has a paragraph about how to implement hashcode and equals for entities that should give you a better explanation.
Another couple of things I've noticed:
I don't think you need to add #RegisterForReflection. That's only helpful in specific circumstances and this shouldn't be one of them.
Many-to-one associations are usually initialized with null. That's because you can check if the association exists using a null check. I suspect it may also cause errors because you are associating a new unmanaged object.
Related
I need to create a table in my db where I can save Filters so I created this class like this:
#Entity
class Filter private constructor(
val name: String?,
#TypeConverters(Converters::class)
val sortBy: Sort?,
val withGenres: List<Int>?,
val withCast: List<Int>?,
) : Serializable {
#PrimaryKey(autoGenerate = true)
var id: Int? = null
data class Builder(
var name: String? = null,
var sortBy: Sort? = null,
var withGenres: List<Int> = listOf(),
var withCast: List<Int> = listOf(),
) {
fun name(name: String) = apply { this.name = name }
fun sortBy(sort: Sort?) = apply { this.sortBy = sort }
fun withGenres(genresId: List<Int>) = apply { this.withGenres = genresId }
fun withCast(castIds: List<Int>) = apply { this.withCast = castIds }
fun build() = Filter(name, sortBy, withGenres, withCast)
}
/**
* Sorting options, default order is Descendant (desc)
*/
enum class Sort {
POPULARITY, RELEASE_DATE, REVENUE, VOTE_AVERAGE;
private var order: Order = Order.DESC
override fun toString(): String {
return this.name.lowercase() + "." + this.order.name.lowercase()
}
fun setOrder(order: Order) {
this.order = order
}
enum class Order {
ASC, DESC
}
}
override fun toString(): String {
return "sortBy: ${sortBy.toString()}, withGenres: $withGenres"
}
}
But upon compilation I get the following errors:
Cannot configure how to save field withGenres into database
Cannot configure how to save field withCast into database
Entities and POJOs must have public constructor
Cannot find setter for field (all the fields)
How can I modify the code so that it works, what are the problems?
Is there an alternative to builder pattern in Kotlin that I don't need to rewrite all attributes?
I read a lot of discussions about this, but none conclusive enough nor something that helps me.
I'm trying to do something like this, but it doesn't work because of the private set:
import java.time.OffsetDateTime
import java.time.OffsetDateTime.now
import java.util.UUID
import java.util.UUID.randomUUID
data class UserEntity(
val id: UUID = randomUUID(),
) {
var firstName: String = ""
private set
var lastName: String = ""
private set
var mainEmail: String = ""
private set
var isActive: Boolean = true
private set
var createdAt: OffsetDateTime = now()
private set
companion object {
fun new(id: UUID = randomUUID(), block: UserEntity.() -> Unit): UserEntity {
return UserEntity(id).apply(block).apply{this.validate()}
}
}
fun update(block: UserEntity.() -> Unit) {
this.apply(block).apply{this.validate()}
}
private fun validate(){
// validations and throw errors here
}
}
//============================================================
// to use like this
val userEntity = UserEntity.new {
firstName = "User"
lastName = "Last"
mainEmail = "user.last#email.com"
}
userEntity.update {
firstName = "Qwerty"
lastName = "Another"
mainEmail = "qwerty.another#email.com"
}
There is a copy method. In Kotlin data classes should be rather immutable, look at this example:
data class User(
val id: UUID = UUID.randomUUID(),
val name: String,
val isActivated: Boolean = false
) {
fun activate(): User {
return this.copy(isActivated = true)
}
}
val user = User(name = "John")
val withNewName = user.copy(name = "Kate")
val activatedKate = withNewName.activate()
Using the sample below (based on this source but with some changes), if the property #Id is named "id", the Neo4J (latest version) does not recognizes the attribute when the load is executed. But if I named it to any other name, such as "myId", the method works. Please, does anyone know why?
My code:
package sample
import org.neo4j.ogm.annotation.Id
import org.neo4j.ogm.annotation.NodeEntity
import org.neo4j.ogm.annotation.Relationship
import org.neo4j.ogm.config.Configuration
import org.neo4j.ogm.session.SessionFactory
#NodeEntity
data class Actor(
#Id
var id: Long?,
var name:String
) {
constructor() : this(-1, "")
#Relationship(type = "ACTS_IN", direction = "OUTGOING")
var movies = mutableSetOf<Movie>()
fun actsIn(movie: Movie) {
movies.add(movie)
movie.actors.add(this)
}
}
#NodeEntity
data class Movie(
#Id
var id: Long?,
var title:String,
var released:Int
) {
constructor() : this(-1L, "", -1)
#Relationship(type = "ACTS_IN", direction = "INCOMING")
var actors = mutableSetOf<Actor>()
}
class Sample {
private val uri = "bolt://localhost:7687"
val configuration = Configuration.Builder()
.uri(uri)
//.credentials("neo4j", "test")
.build()
val sessionFactory = SessionFactory(configuration, "sample")
fun save() {
val pack = SessionFactory::class.java.name
val l = java.util.logging.Logger.getLogger(pack)
l.setLevel(java.util.logging.Level.ALL)
val session = sessionFactory.openSession()
val develsAdvocate = Movie(1, "The Devil’s Advocate", 1997)
val theMatrix = Movie(2, "The Matrix", 1999)
val act = Actor(1, "Carrie-Anne Moss")
act.actsIn(theMatrix)
val keanu = Actor(2, "Keanu Reeves")
keanu.actsIn(theMatrix)
keanu.actsIn(develsAdvocate)
session.save(develsAdvocate)
session.save(theMatrix)
session.clear()
// ** Only works if the entity has a attribute #Id with name different as "id"
val theMatrixReloaded = session.load(Movie::class.java, theMatrix.id, 2)
println("Found ${theMatrixReloaded.title}")
for (actor in theMatrixReloaded.actors) {
println("Actor ${actor.name} acted in ${actor.movies.joinToString { it.title }}")
}
sessionFactory.close()
}
}
fun main(args: Array<String>) {
val neoInstance = Sample()
neoInstance.save()
}
I am making a toDoList app with multiple users. I have two entities. One is User and the other one is Task
#Entity(tableName = "tasks")
data class Task(
#PrimaryKey(autoGenerate = true)
val id : Int,
val taskUserId : Int,
val taskName : String,
val taskDescription : String,
var taskPriority : Int)
#Entity(tableName = "users")
data class User(
#PrimaryKey
val userId : Int,
val userName : String,
val password : String)
As you can see the two entities need to have one thing in common and that would be the users id so I could query for tasks based on the users id. This is what my dao looks like:
#Dao
interface UserDao {
#Query("SELECT * FROM users")
fun getAllUsers() : LiveData<List<User>>
#Query("SELECT * FROM tasks WHERE taskUserId LIKE :userId ORDER BY taskPriority DESC")
fun getAllUsersTasks(userId : Int) : LiveData<List<Task>>
#Insert
suspend fun saveUser(user : User)
#Insert
suspend fun saveTask(task : Task)
}
And here are the other model elements:
class TaskRepository(private val userDao: UserDao) {
var userId : Int = 0
val allTasks = userDao.getAllUsersTasks(userId)
val allUsers : LiveData<List<User>> = userDao.getAllUsers()
suspend fun saveUser(user : User){
userDao.saveUser(user)
}
suspend fun saveTask(task : Task){
userDao.saveTask(task)
}
}
And the ViewModel:
class MainViewModel(application: Application) : AndroidViewModel(application) {
private val taskRepository : TaskRepository
val allTasks : LiveData<List<Task>>
val allUsers : LiveData<List<User>>
private var userId : Int = 0
init {
val taskDao = UserTasksDatabase.getDatabase(application)!!.userDao()
taskRepository = TaskRepository(taskDao)
allTasks = taskRepository.allTasks
allUsers = taskRepository.allUsers
taskRepository.userId = this.userId
}
fun saveUser(user: User) = viewModelScope.launch(Dispatchers.IO){
taskRepository.saveUser(user)
}
fun saveTask(task: Task) = viewModelScope.launch(Dispatchers.IO){
taskRepository.saveTask(task)
}
fun setUserId(id : Int){
this.userId = id
}
fun getUserId() : Int{
return this.userId
}
}
When the user pushes the login or the sign up button I set the user's id in the viewModel
logInButton.setOnClickListener {
val userName: String = userNameEditText.text.toString()
val password: String = passwordEditText.text.toString()
val id: Int = IdMaker.generateId(userName)
val user = User(id, userName, password)
if(listOfAllUsers.contains(user)){
sharedViewModel.setUserId(id)
Toast.makeText(requireContext(), "Welcome $userName", Toast.LENGTH_LONG).show()
fragmentManager!!.beginTransaction().apply {
replace(R.id.fl_activity_main, MainFragment())
commit()
}
}else{
Toast.makeText(requireContext(), "The user already exists", Toast.LENGTH_LONG).show()
}
}
But when i try to access the users in the main fragment and set them on my recyclerview I get nothing. And I can't really see what am I missing.
sharedViewModel.allTasks.observe(this, Observer {tasks ->
tasks.let { adapter.setTasks(it) }
})
I'm sorry if a question is a bit simple but I hope I will gain some insight in what am I missing.
Side note: The recyclerview is set up properly and tested.
I think you are not setting the TaskRepository.userId correctly.
class MainViewModel(application: Application) : AndroidViewModel(application) {
private val taskRepository : TaskRepository
private var userId : Int = 0
init {
...
// init is called only once at the start of this viewModel's instance creation
// Here, you set your repository's userId to 0
taskRepository.userId = this.userId
}
}
At the instance creation of your MainViewModel, you set the userId of its TaskRepository instance to 0, which in your following codes is not changed anymore. Upon your login button click, you do call:
sharedViewModel.setUserId(id)
but the function alters only the viewModel's userId property, not the userId property of your TaskRepository instance. So try:
class MainViewModel(application: Application) : AndroidViewModel(application) {
private lateinit var taskRepository : TaskRepository
val allTasks = taskRepository.allTasks
val allUsers = taskRepository.allUsers
// remove init {}
fun setUserId(id : Int){
// set up the repository only after userId is known
val taskDao = UserTasksDatabase.getDatabase(application)!!.userDao()
taskRepository = TaskRepository(taskDao, id)
}
}
And alter your TaskRepository class:
class TaskRepository(private val userDao: UserDao, private val userId: Int) {
val allTasks: LiveData<List<Task>>
get() = userDao.getAllUsersTasks(userId)
val allUsers: LiveData<List<User>>
get() = userDao.getAllUsers()
...
}
I tried to increate the performance of my SpringBoot app with MariaDB.
I logged the sql and analyze it and figure it out that the FetchType.LAZY is not respected.
I have a MainEntity class
#MappedSuperclass
#DynamicUpdate
open class MainEntity {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(columnDefinition = "BIGINT UNSIGNED")
var id: Long = 0
var createdAt: Timestamp? = null
var updatedAt: Timestamp? = null
#ManyToOne(fetch = FetchType.LAZY)
var createdBy: UserStaff? = null
#ManyToOne(fetch = FetchType.LAZY)
var updatedBy: UserStaff? = null
#PrePersist
fun prePersist() {
val loggedInUser = SecurityContextHolder.getContext().authentication?.credentials
if(loggedInUser is UserStaff) {
createdBy = createdBy ?: loggedInUser
}
}
#PreUpdate
fun preUpdate() {
updatedAt = Timestamp(System.currentTimeMillis())
val loggedInUser = SecurityContextHolder.getContext().authentication?.credentials
if(loggedInUser is UserStaff) {
updatedBy = loggedInUser
}
}
}
where UserStaff entity:
#Entity
class UserStaff : MainEntity(), UserDetails {
var fullName: String? = null
#Column(name = "username")
var uname: String? = null
#Column(name = "password")
var pwd: String? = null
..... more details
}
and then I have another Entity that inherit MainEntity
#Entity
class CashMutation: MainEntity() {
#ManyToOne(fetch = FetchType.LAZY)
var from: Office? = null
#ManyToOne(fetch = FetchType.LAZY)
#JsonIgnore
var to: Office? = null
#Column(columnDefinition = "DECIMAL(20,5)")
var value: BigDecimal? = null
#ManyToOne(fetch = FetchType.LAZY)
#JsonIgnore
var approvedBy: UserStaff? = null
#ManyToOne(fetch = FetchType.LAZY)
#JsonIgnore
var sendBy: UserStaff? = null
#ManyToOne(fetch = FetchType.LAZY)
var status: MutationStatus? = null
var remark: String? = null
}
#Repository
interface CashMutationKasRepository : JpaRepository<CashMutation, Long>, JpaSpecificationExecutor<CashMutation> {
fun findAllByStatusIdNot(id: Long) : List<CashMutation>
}
Then I have a RestController
#GetMapping("/test/20")
fun test20() {
mutationRepository.findAll().forEach {
println(it.id)
}
}
When I look at the sql log, it also query the createdBy of the CashMutation and then also querty the createdBy of the UserStaff until it is null. It also queries the Office including the createdBy inside Office (since all my entities inherit MainEntity)
I looked around and found some similar problems but their solution does not work for me (for example, set spring.jpa.open-in-view=false in my properties). I tried to use #Transactional(readOnly = true) but it also dies not work.
Thank you and cheers!