I am new to android Room and trying to insert some one-to-many relationships into my database. But I am facing some issues that I have not managed to fix sofar.
Here is my data constellation:
Entity:
#Entity(tableName = "artist")
data class Artist(
#PrimaryKey(autoGenerate = true) val id: Long = 0,
val name: String,
) {
}
#Entity(
tableName = "song",
foreignKeys = [ForeignKey(
entity = Artist::class,
parentColumns = ["id"],
childColumns = ["artistId"],
onDelete = ForeignKey.CASCADE
)]
)
data class Song(
#PrimaryKey(autoGenerate = true) val id: Long = 0,
val artistId: Long,
val title: String?
) {
}
data class ArtistWithSongs(
#Embedded val artist: Artist,
#Relation(parentColumn = "id", entityColumn = "artistId", ) val songs: List<Meal>
) {
}
Repository:
#Singleton
class AppRepository #Inject constructor(
private val artistDao: ArtistDao
) {
suspend fun insert(artist: Artist) {
artistDao.insert(artist)
}
}
Dao:
#Dao
interface ArtistDao {
#Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(artist: Artist)
}
ViewModel:
#HiltViewModel
class ArtistViewModel #Inject constructor(
private val repository: AppRepository,
private val savedStateHandle: SavedStateHandle
) : ViewModel() {
fun insert(artist: Artist) = viewModelScope.launch {
repository.insert(artist)
}
}
Then in my activity I call:
private val viewModel: ArtistViewModel by viewModels()
...
val artist = Artist("Bob")
viewModel.insert(artist)
But insert artist does not work. The database is still empty.
Thanks
I was using viewModel.artist.value to check if any artist was present. As the method returning artist has LiveData as return type, I was getting null.
Observing the artist instead proved that my insert method is working just right.
Related
I am trying to simply test a repository. Here is my test code:
#ContextConfiguration(classes = [
DatabaseTestConfiguration::class
])
#TestPropertySource(
properties = [
"spring.jpa.hibernate.ddl-auto=create-drop",
"spring.flyway.enabled=false"
]
)
#AutoConfigureTestDatabase
#AutoConfigureDataJpa
#AutoConfigureTestEntityManager
internal open class UserTest {
#Autowired
private lateinit var entityManager: TestEntityManager
#Autowired
private lateinit var userRepository: UserRepository
#Test
#Transactional
open fun whenSuccessfullyFoundAnyDuplicatedUserThenReturnTrue() {
val user = User()
user.name = NAME
user.surname = SURNAME
user.gender = GENDER
entityManager.persistAndFlush(user)
assertEquals(true, userRepository.anyDuplicate(
name = user.name,
surname = user.surname,
gender = user.gender,
))
}
companion object {
const val NAME = "John"
const val SURNAME = "Snow"
const val GENDER = "male"
}
Here is my DatabaseTestConfiguration:
#TestConfiguration
#EntityScan(value = [
"com.username.db.entities"
])
#EnableJpaRepositories(
"com.username.db.repositories"
)
class DatabaseTestConfiguration {
}
This is my repository:
#Repository
interface UserRepository: JpaRepository<User,Long> {
#Query(
"""
SELECT CASE
WHEN COUNT(user.id) > 0 THEN TRUE
ELSE FALSE END
FROM User user
WHERE user.name = :name AND user.surname = :surname AND user.gender = :gender
)
"""
)
fun anyDuplicate(
#Param("name") name: String?,
#Param("surname") surname: String?,
#Param("gender") gender: String?
): Boolean
}
This is my entity:
#Entity
#Table(name = "user")
class User {
#Id
#GeneratedValue(
strategy = GenerationType.SEQUENCE,
generator = "cdr_generator"
)
#SequenceGenerator(
name = "cdr_generator",
sequenceName = "cdr_seq",
allocationSize = 1
)
#Column(name = "id", nullable = false)
var id: Long? = null
#Column(name = "name", nullable = false)
var name: String? = null
#Column(name = "surname", nullable = false)
var surname: String? = null
#Column(name = "gender", nullable = false)
var gender: String? = null
}
Now when i am testing. this will always fail my test. When i debug it, i get that in my test it never enters userRepository.anyDuplicate() function. I assume that the problem is with configuring test. Can anybody help?
I am new to Stack Overflow, so sorry in advance if I make a mistake regarding asking a question.
I am trying to create a one-to-many relation between my User entity and my Team entity. Which is pretty straight forward as you can auto-generate a join table's using annotions. But I also want an extra column in this join table. I want to keep track of whether someone is a coach yes or no. I hope this ERD gives a clearer idea of what I mean:
As you can see, I also added the join table user_roles. I think this should be taken into consideration when considering how to solve this problem. Because the User Entity has a set of these roles. These (should) be retrieved when you retrieve a Team from the database.
This is what I have now in terms of code:
User class:
package com.goldwasser.app.resourceserver.entity
import java.time.LocalDateTime
import java.util.*
import javax.persistence.*
import javax.validation.constraints.Email
import javax.validation.constraints.NotBlank
import javax.validation.constraints.Size
#Entity
#Table(name = "users")
data class User(
#NotBlank
#Size(max = 50)
#Email
private val username: String,
#NotBlank
#Size(max = 50)
#Email
private val email: String,
#NotBlank
#Size(max = 120)
private val password: String,
#NotBlank
#Size(max = 50)
private val firstname: String,
#NotBlank
#Size(max = 50)
private val lastname: String,
#NotBlank
private val createTime: LocalDateTime,
#ManyToMany(fetch = FetchType.EAGER)
#JoinTable(name = "user_roles",
joinColumns = [JoinColumn(name = "user_id")],
inverseJoinColumns = [JoinColumn(name = "role_id")])
private val roles: MutableSet<Role>,
#OneToOne(mappedBy = "player", cascade = [CascadeType.ALL], orphanRemoval = true)
var playerTeam: PlayerTeam? = null
) : AbstractJpaPersistable<UUID>() {
companion object {
operator fun invoke(
username: String? = null,
email: String? = null,
password: String? = null,
firstName: String? = null,
lastName: String? = null,
createTime: LocalDateTime? = null,
roles: MutableSet<Role>? = null
) = User(
username ?: "",
email ?: "",
password ?: "",
firstName ?: "",
lastName ?: "",
createTime ?: LocalDateTime.now(),
roles ?: mutableSetOf()
)
}
fun getRoles(): Set<Role> = roles
fun getPassword(): String = password
fun getUsername(): String = username
fun getEmail(): String = email
fun getFirstname(): String = firstname
fun getLastname(): String = lastname
fun getCreateTime(): LocalDateTime = createTime
fun setRoles(roles: Set<Role>) {
this.roles.addAll(roles)
}
}
PlayerTeam class:
package com.goldwasser.app.resourceserver.entity
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.databind.SerializationFeature
import java.util.UUID
import javax.persistence.*
#Entity
#Table(name = "player_teams")
data class PlayerTeam(
#EmbeddedId
var playerTeamId: PlayerTeamId? = null,
#OneToOne(fetch = FetchType.LAZY)
#MapsId("userId")
val player: User? = null,
#ManyToOne(fetch = FetchType.LAZY)
#MapsId("teamId")
val team: Team? = null,
val isCoach: Boolean = false
) {
override fun toString(): String {
val mapper = ObjectMapper()
mapper.enable(SerializationFeature.INDENT_OUTPUT)
return mapper.writeValueAsString(this)
}
fun getIsCoach(): Boolean = isCoach
}
PlayerTeamId (Composite-key) class:
package com.goldwasser.app.resourceserver.entity
import org.hibernate.annotations.GenericGenerator
import org.hibernate.annotations.Type
import java.io.Serializable
import java.util.UUID
import javax.persistence.Column
import javax.persistence.Embeddable
import javax.persistence.GeneratedValue
#Embeddable
data class PlayerTeamId (
#GeneratedValue(generator = "uuid2")
#GenericGenerator(
name = "uuid2",
strategy = "uuid2"
)
#Column(name = "user_id", updatable = false, nullable = false, columnDefinition = "VARCHAR(36)")
#Type(type = "uuid-char")
var userId: UUID = UUID.randomUUID(),
#GeneratedValue(generator = "uuid2")
#GenericGenerator(
name = "uuid2",
strategy = "uuid2"
)
#Column(name = "team_id", updatable = false, nullable = false, columnDefinition = "VARCHAR(36)")
#Type(type = "uuid-char")
var teamId: UUID = UUID.randomUUID()
) : Serializable
My Team class:
package com.goldwasser.app.resourceserver.entity
import java.util.UUID
import javax.persistence.*
#Entity
#Table(name="teams")
data class Team(
val name: String,
#OneToMany(mappedBy = "team", cascade = [CascadeType.ALL], orphanRemoval = true)
var playerTeams: MutableSet<PlayerTeam>? = null
): AbstractJpaPersistable<UUID>() {
}
And at last, my main Entity class (all entities should inherit this one because it provides them with an Id as UUID):
package com.goldwasser.app.resourceserver.entity
import org.hibernate.annotations.GenericGenerator
import org.hibernate.annotations.Type
import org.springframework.data.util.ProxyUtils
import java.io.Serializable
import javax.persistence.*
#MappedSuperclass
abstract class AbstractJpaPersistable<T : Serializable> {
#Id
#GeneratedValue(generator = "uuid2")
#GenericGenerator(
name = "uuid2",
strategy = "uuid2"
)
#Column(name = "id", updatable = false, nullable = false, columnDefinition = "VARCHAR(36)")
#Type(type = "uuid-char")
private var id: T? = null
fun getId(): T? {
return this.id
}
fun setId(type: T?) {
this.id = type
}
override fun equals(other: Any?): Boolean {
other ?: return false
if (this === other) return true
if (javaClass != ProxyUtils.getUserClass(other)) return false
other as AbstractJpaPersistable<*>
return if (null == this.id) false else this.id == other.id
}
override fun hashCode(): Int {
return 31
}
override fun toString() = "Entity of type ${this.javaClass.name} with id: $id"
}
Now that you've seen all my entity classes, I will show/tell you where I have been getting stuck at.
I have a repository called "TeamRepository". It's responsible for handling all database action related stuff for the Team entity:
package com.goldwasser.app.resourceserver.repository
import com.goldwasser.app.resourceserver.entity.Team
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.stereotype.Repository
import java.util.UUID
#Repository
interface TeamRepository : JpaRepository<Team, UUID> {
}
And I have a service called "TeamServiceImpl":
package com.goldwasser.app.resourceserver.service
import com.goldwasser.app.resourceserver.entity.PlayerTeam
import com.goldwasser.app.resourceserver.entity.PlayerTeamId
import com.goldwasser.app.resourceserver.entity.Team
import com.goldwasser.app.resourceserver.entity.User
import com.goldwasser.app.resourceserver.repository.PlayerTeamRepository
import com.goldwasser.app.resourceserver.repository.TeamRepository
import com.goldwasser.app.resourceserver.repository.UserRepository
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.data.annotation.Persistent
import org.springframework.stereotype.Service
import javax.transaction.Transactional
#Service
class TeamServiceImpl : TeamService {
#Autowired
private lateinit var playerTeamRepository: PlayerTeamRepository
#Autowired
private lateinit var userRepository: UserRepository
#Autowired
private lateinit var teamRepository: TeamRepository
#Transactional
override fun addPlayerToTeam(player: User, team: Team) {
val playerTeamId = PlayerTeamId(
player.getId()!!,
team.getId()!!
)
val playerTeam = PlayerTeam(
playerTeamId,
user = player,
team = team,
isCoach = true
)
playerTeamRepository.save(playerTeam)
}
override fun getPlayerTeam(team: Team) : Team {
val teamInDb = teamRepository.findById(team.getId()!!).get()
return teamInDb;
}
}
The problem is that when I call getPlayerTeam(team), my teamInDb does contain a Team-object, but the playerTeams of the Team-object results in a LazyInitializationException. The same applies when I try to fetch a user object. I know this has to do something with JPA not being able to access the requested entity because of the session being closed. But I do not know how to fix this.
EDIT:
I have added the all-open plugin:
plugins {
id("org.springframework.boot") version "2.6.6"
id("org.jetbrains.kotlin.plugin.allopen") version "1.6.21"
id("io.spring.dependency-management") version "1.0.11.RELEASE"
kotlin("jvm") version "1.6.10"
kotlin("plugin.spring") version "1.6.10"
kotlin("plugin.jpa") version "1.6.10"
}
Now I do get a PlayerTeam object containing a Player and Team. But those Player and Team have also have a PlayerTeam set which cannot be initialised, see picture below:
I am fairly new to Spring Boot and unable to find out why my CRUD repository seems to keep throwing an exception saying "Operation is not supported for read-only collection" when sending a request to a Put endpoint. All other repositories seem to work just fine. Here's my code:
User.kt
package com.karbal.tutortek.entities
import com.karbal.tutortek.dto.userDTO.UserPostDTO
import java.sql.Date
import javax.persistence.*
#Entity
#Table(name = "users")
data class User(
#Id
#Column(name = "id", nullable = false)
#GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "user_generator")
#SequenceGenerator(name = "user_generator", sequenceName = "user_seq", allocationSize = 1)
var id: Long? = null,
#Column(name = "firstName", nullable = false)
var firstName: String = "",
#Column(name = "lastName", nullable = false)
var lastName: String = "",
#Column(name = "birthDate", nullable = false)
var birthDate: Date = Date(System.currentTimeMillis()),
#Column(name = "rating", nullable = false)
var rating: Float = 0.0F,
#OneToMany(mappedBy = "user")
var payments: List<Payment> = listOf(),
#OneToMany(mappedBy = "user")
var topics: List<Topic> = listOf()
){
constructor(userPostDTO: UserPostDTO) : this(
null,
userPostDTO.firstName,
userPostDTO.lastName,
userPostDTO.birthDate,
userPostDTO.rating
)
fun copy(user: User){
firstName = user.firstName
lastName = user.lastName
birthDate = user.birthDate
rating = user.rating
payments = user.payments
topics = user.topics
}
}
UserController.kt
package com.karbal.tutortek.controllers
import com.karbal.tutortek.dto.userDTO.UserGetDTO
import com.karbal.tutortek.dto.userDTO.UserPostDTO
import com.karbal.tutortek.entities.User
import com.karbal.tutortek.services.UserService
import org.springframework.http.HttpStatus
import org.springframework.web.bind.annotation.*
import org.springframework.web.server.ResponseStatusException
import java.util.*
#RestController
class UserController(val userService: UserService) {
#PostMapping("/users/add")
fun addUser(#RequestBody userDTO: UserPostDTO): UserGetDTO {
val user = User(userDTO)
return UserGetDTO(userService.saveUser(user))
}
#DeleteMapping("/users/{id}")
fun deleteUser(#PathVariable id: Long){
val user = userService.getUser(id)
if(user.isEmpty) throw ResponseStatusException(HttpStatus.NOT_FOUND, "User not found")
userService.deleteUser(id)
}
#GetMapping("/users/all")
fun getAllUsers() = userService.getAllUsers().map { u -> UserGetDTO(u) }
#GetMapping("/users/{id}")
fun getUser(#PathVariable id: Long): UserGetDTO {
val user = userService.getUser(id)
if(user.isEmpty) throw ResponseStatusException(HttpStatus.NOT_FOUND, "User not found")
return UserGetDTO(user.get())
}
#PutMapping("/users/{id}")
fun updateUser(#PathVariable id: Long, #RequestBody userDTO: UserPostDTO){
val user = User(userDTO)
val userInDatabase = userService.getUser(id)
if(userInDatabase.isEmpty) throw ResponseStatusException(HttpStatus.NOT_FOUND, "User not found")
val extractedUser = userInDatabase.get()
extractedUser.copy(user)
userService.saveUser(extractedUser)
}
}
UserService.kt
package com.karbal.tutortek.services
import com.karbal.tutortek.entities.User
import org.springframework.stereotype.Service
import com.karbal.tutortek.repositories.UserRepository
#Service
class UserService(val database: UserRepository) {
fun getAllUsers(): List<User> = database.getAllUsers()
fun saveUser(user: User) = database.save(user)
fun deleteUser(id: Long) = database.deleteById(id)
fun getUser(id: Long) = database.findById(id)
}
UserRepository.kt
package com.karbal.tutortek.repositories
import com.karbal.tutortek.entities.User
import org.springframework.data.jpa.repository.Query
import org.springframework.data.repository.CrudRepository
import org.springframework.stereotype.Repository
#Repository
interface UserRepository : CrudRepository<User, Long> {
#Query("SELECT * FROM users", nativeQuery = true)
fun getAllUsers(): List<User>
}
UserPostDTO.kt
package com.karbal.tutortek.dto.userDTO
import java.sql.Date
data class UserPostDTO(
var firstName: String,
var lastName: String,
var rating: Float,
var birthDate: Date
)
The JSON that I send:
{
"firstName": "Thomas",
"lastName": "Thompson",
"rating": 4.7,
"birthDate": "2000-02-03"
}
Post works fine. Put works fine on other entities in my code. But here it always responds with 500 and a message "Operation is not supported for read-only collection". Any ideas why this could be happening?
Just solved this after a couple of hours. Changed lists in User class to mutable lists and now it works fine.
I'm creating a test and basically doing different transactions inside a #Transactional method.
I add a Project, then add a Task to it, and last will fetch the project again from DB to test it has the task saved.
Please note the case I'm showing is a unit test but I'm more interesting in fixing the transactional methods and not the test itself as I already had this in the past in "production code".
Model Classes:
#Entity
#Table(name = "Task")
data class Task(
#Id
#SequenceGenerator(name = "TaskSeq", sequenceName = "TaskSeq", initialValue = 100)
#GeneratedValue(generator = "TaskSeq")
val id: Long = 0,
#Column(nullable = false)
val name: String,
val description: String,
val inZ: LocalDateTime = LocalDateTime.now(),
var outZ: LocalDateTime = JpaConstants.MAX_DATETIME,
var completed: Boolean = false,
#ManyToOne(cascade = [CascadeType.PERSIST, CascadeType.MERGE, CascadeType.REFRESH])
#JoinColumn(name = "projectId")
var project: Project? = null
) {
}
#Entity
#Table(name = "Project")
data class Project(
#Id
#SequenceGenerator(name = "ProjectSeq", sequenceName = "ProjectSeq", initialValue = 100)
#GeneratedValue(generator = "ProjectSeq")
val id: Long = 0,
#Column(nullable = false)
var name: String,
#OneToMany(mappedBy = "project", cascade = [CascadeType.MERGE, CascadeType.PERSIST, CascadeType.REFRESH])
val tasks: MutableList<Task> = Lists.mutable.empty()
) {
}
Service Classes:
#Service
class ProjectServiceImpl(private val projectRepository: ProjectRepository) : ProjectService {
override fun save(project: Project): Project {
return projectRepository.save(project)
}
}
#Service
class TaskServiceImpl(private val taskRepository: TaskRepository, private val projectRepository: ProjectRepository) : TaskService {
override fun save(task: Task): Task {
return taskRepository.save(task)
}
override fun addTaskToProject(projectId: Long, task: Task): Task {
val project = projectRepository.findById(projectId).orElseThrow { RecordNotFoundException("Couldn't find project with id {$projectId}") }
task.project = project
return save(task)
}
}
The class I'm trying to use the transactional method:
class TaskServiceImplTest : TaskApplicationTests() {
#Autowired
private lateinit var taskService: TaskService
#Autowired
private lateinit var taskRepository: TaskRepository
#Autowired
private lateinit var projectService: ProjectService
#Test
#Transactional
fun canInsertTaskToProject() {
val project = projectService.save(Project(name = "Conquer Paris"))
var task = Task(name = "Check how many people we need to hire", description = "")
task = taskService.addTaskToProject(project.id, task)
assertTrue(task.id > 0)
val projects = projectService.findAll()
assertEquals(1, projects.size())
assertEquals(1, projects[0].tasks.size)
assertEquals(task.id, projects[0].tasks[0].id)
}
If I add a #Transactional(REQUIRES_NEW) to the methods in the service it will work, but I don't want it as if this method is called inside a real transaction I want it to be rolled back accordingly. Also I'd like to avoid using too many REQUIRES_NEW to avoid future problems
If I remove the #Transactional from the test method, it won't work when I test the size of the task list on last two lines as they are lazy.
What is the best way to make it work ? I thought that inside a #Transactional when I used another command from db it would get the latest updates that were not committed yet..
If needed, code in Java is fine too :)
Thanks in advance!
Based on your scenarios, you can use #TestEntityManagerso that each test can be managed in transaction context.
This example can help you,
https://grokonez.com/testing/datajpatest-with-spring-boot
I need to save 2 models at the same time in the transaction, but...
org.postgresql.util.PSQLException: ERROR: null value in column "book_id" violates not-null constraint
I don't understand what is going wrong. My models:
a) Chapter
#Entity
class Chapter() : AuditModel() {
constructor(number: Int, title: String) : this() {
this.number = number
this.title = title
}
#Column(nullable = false)
var number: Int? = null
#Column(nullable = false)
lateinit var title: String
#Column(nullable = false)
var progress: Int = 0
#ManyToOne
#JoinColumn(name = "book_id")
lateinit var book: Book
}
b) Book
#Entity
class Book() : AuditModel() {
constructor(title: String, author: String) : this() {
this.title = title
this.author = author
}
#Column(nullable = false)
lateinit var title: String
#Column(nullable = false)
lateinit var author: String
#OneToMany(mappedBy = "book", cascade = [CascadeType.PERSIST])
val chapters: MutableSet<Chapter> = HashSet()
}
And function where I save models:
#Transactional
fun createBook(title: String, author: String): Boolean {
val book = Book(title, author)
val chapter = Chapter(1, "Example - 1")
book.chapters.add(chapter)
return bookRepository.save(book) != null
}
How to fix it? I'm new in Spring and it's totally incomprehensible to me.