Save child and parent at the same time - spring

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.

Related

How to do update?

I have these entities
#Entity(name = "inspiration")
class InspirationEntity(
#Id
var uuid: UUID? = null,
#Column(name = "display_name")
var displayName: String,
#CreationTimestamp
#Column(name = "created_at")
#Temporal(TemporalType.TIMESTAMP)
var createdAt: Date?,
#UpdateTimestamp
#Temporal(TemporalType.TIMESTAMP)
#Column(name = "last_modified_date")
var lastModifiedDate: Date?,
#OneToMany(targetEntity = BaseSliderEntity::class)
#Cascade(CascadeType.ALL)
var sliderList: List<BaseSliderEntity>,
)
#Entity(name = "base_slider")
#Inheritance(strategy = InheritanceType.JOINED)
//TODO investigate DiscriminatorColumn anotation and best practices, because it shows some warning in logs
#DiscriminatorColumn(name = "slider_type", discriminatorType = DiscriminatorType.STRING)
abstract class BaseSliderEntity(
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
open var id: Long? = null,
)
#Entity(name = "dish_of_the_day")
#DiscriminatorValue("DISH_OF_THE_DAY")
class DishOfTheDayEntity(
#Column(name = "title_en")
var titleEN: String,
#Column(name = "title_de")
var titleDE: String,
) : BaseSliderEntity()
#Entity(name = "inspiration_screen_link")
#DiscriminatorValue("LINK")
class InspirationScreenLinkEntity(
#Enumerated(EnumType.STRING)
var destination: Destination,
) : BaseSliderEntity()
this is my dto
data class InspirationDTO(
#JsonProperty(access = JsonProperty.Access.READ_ONLY)
var uuid: UUID?,
#JsonProperty(access = JsonProperty.Access.READ_ONLY)
var createdAt: Date?,
#JsonProperty(access = JsonProperty.Access.READ_ONLY)
var lastModifiedDate: Date?,
val displayName: String,
val inspirationScreenItemList: List<InspirationScreenItemDTO>,
)
and this is InspirationScreenItemDTO
#JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type")
#JsonSubTypes(
JsonSubTypes.Type(value = DishOfTheDayDTO::class, name = "DishOfTheDay"),
JsonSubTypes.Type(value = InspirationScreenContinuousSliderDTO::class, name = "InspirationScreenContinuousSlider"),
JsonSubTypes.Type(value = InspirationScreenLinkDTO::class, name = "InspirationScreenLink"),
JsonSubTypes.Type(value = InspirationScreenPagingSliderDTO::class, name = "InspirationScreenPagingSlider"),
JsonSubTypes.Type(
value = InspirationScreenRecentlyViewedSliderDTO::class,
name = "InspirationScreenRecentlyViewedSlider"
),
JsonSubTypes.Type(value = InspirationScreenTagsSliderDTO::class, name = "InspirationScreenTagsSlider"),
)
open class InspirationScreenItemDTO
when I try to update
like this
#Transactional
fun update(uuid: UUID, inspirationDTO: InspirationDTO): InspirationDTO {
var entity = inspirationRepository.findById(id).get()
val updatedEntity: InspirationEntity = inspirationMapper.convertToEntity(inspirationDTO)
entity.sliderList = updatedEntity.sliderList
val result: InspirationEntity = inspirationRepository.save(entity)
return inspirationMapper.convertToDto(result)
}
this is my swagger post
{
"displayName": "ED",
"inspirationScreenItemList": [
{
"type": "DishOfTheDay",
"titleEN": "GGGGGGG",
"titleDE": "GGGGGGG"
}
]
}
in DishofDto table it creates two rows first and updated, and inspiration remains one row which is okay but dish of the day shouldn't contain two rows, it should be just updated one.
My solution was to add uuid id as primary key in inspiration which works with deleting by id and then put toUpdateDto under the same id and save , but I'm not sure it's good solution.

Spring Data JPA Repository function does not work in Test

I have a question regarding Spring Data JPA.
To make it as simple as possible I made up a very simple example.
We have the TestUser, that can have a FavouriteColor, but his favouriteColor can also be null.
TestUser.kt
#Entity
class TestUser(
#Id
#Column(name = "TestUserId")
var userId: Long,
#Column(name = "Name")
var name: String,
#Column(name = "FavouriteColorId")
var favouriteColorId: Long? = null,
#OneToOne
#JoinColumn(
name = "FavouriteColorId",
referencedColumnName = "FavouriteColorId",
insertable = false,
updatable = false,
nullable = true
)
var favouriteColor: FavouriteColor? = null
)
FavouriteColor.kt
#Entity
class FavouriteColor(
#Id
#Column(name = "FavouriteColorId")
var favouriteColorId: Long,
#Column(name = "ColorCode")
var colorCode: String
)
When I search for the users that have a favourite Color by findTestUsersByFavouriteColorNotNull(), the size of the result is 0. Even if there is an User that has a favourite color. And when I use findAll() and then apply the filter, the result is correct.
StackOverflowTest.kt
#SpringBootTest
#Transactional
class StackOverflowTest {
#Autowired
lateinit var testUserRepository: TestUserRepository
#Autowired
lateinit var favouriteColorRepository: FavouriteColorRepository
#Test
fun testFilter() {
val favouriteColor = FavouriteColor(favouriteColorId = 0L, colorCode = "#000000")
favouriteColorRepository.save(favouriteColor)
val user = testUserRepository.save(TestUser(userId = 0L, name = "Testuser"))
user.favouriteColor = favouriteColor
testUserRepository.save(user)
val usersWithColor1 = testUserRepository.findAll().filter { it.favouriteColor != null }
assert(usersWithColor1.size == 1) // This assertion is correct
val usersWithColor2 = testUserRepository.findTestUsersByFavouriteColorIdIsNotNull()
assert(usersWithColor2.size == 1) // This assertion fails
val usersWithColor3 = testUserRepository.findTestUsersByFavouriteColorIsNotNull()
assert(usersWithColor3.size == 1) // This assertion fails
}
}
Update:
I added the Repository function findTestUsersByFavouriteColorIdNotNull() but it also does not work
Update2:
I updated the functions to findTestUsersByFavouriteColorIdIsNotNull and findTestUsersByFavouriteColorIsNotNull, but the assertions are still failing
Can somebody explain me, why the findTestUsersByFavouriteColorNotNull() does not work ? And is there some way to get this function working in the tests?
Thanks :)
I'm suspecting that happen because you have 2 variables of the same column name
#Column(name = "FavouriteColorId")
var favouriteColorId: Long? = null,
#OneToOne
#JoinColumn(
name = "FavouriteColorId",
referencedColumnName = "FavouriteColorId",
insertable = false,
updatable = false,
nullable = true
)
var favouriteColor: FavouriteColor? = null
Try removing one of the variable, and try again.

Repository not executing methods in test

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?

Unable to insert entity using room

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.

Manage the update order for queries in JPA

I am creating a simple kanban application as following, each kanban is made out of a sequence of stages and each stage have a level field to define its position. I want to be able to add, move and remove stages at will so I have to keep the level of each stage consistent, simple enough.
#Entity
#Table(name = "kanbans")
data class Kanban (
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "id", nullable = false)
var id: Int? = null,
#get:NotNull
#get:NotBlank
#Column(name = "name", nullable = false)
var name: String? = null,
#get:NotNull
#get:NotBlank
#Column(name = "description", nullable = false)
var description: String? = null,
#get:NotNull
#Column(name = "closed", nullable = false)
var closed: Boolean? = null,
#get:NotNull
#Column(name = "created_at", nullable = false)
var createdAt: LocalDateTime? = null,
#get:NotNull
#Column(name = "updated_at", nullable = false)
var updatedAt: LocalDateTime? = null,
)
#Entity
#Table(name = "stages")
data class Stage (
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "id", nullable = false)
var id: Int? = null,
#get:NotNull
#get:NotBlank
#Column(name = "name", nullable = false)
var name: String? = null,
#get:NotNull
#get:NotBlank
#Column(name = "description", nullable = false)
var description: String? = null,
#get:NotNull
#Column(name = "closed", nullable = false)
var closed: Boolean? = null,
#get:NotNull
#Column(name = "level", nullable = false)
var level: Int? = null,
#OneToMany(fetch = FetchType.LAZY, mappedBy = "stage")
var tasks: List<Task> = ArrayList(),
#get:NotNull
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "kanban_id", nullable = false)
var kanban: Kanban? = null,
#get:NotNull
#Column(name = "created_at", nullable = false)
var createdAt: LocalDateTime? = null,
#get:NotNull
#Column(name = "updated_at", nullable = false)
var updatedAt: LocalDateTime? = null,
)
When creating the first stage its always assigning its level at 0 and then when adding new ones the level will define the stage position at the list of stages. The problem is that when I try to update the previous existing stages to give place to the new one, the only way I found to make this work is to place a saveAndFlush call in a loop but I find it to be not a good ideia.
#Repository
interface StageRepository : JpaRepository<Stage, Int> {
fun findAllByKanbanAndLevelGreaterThanEqualOrderByLevelDesc(kanban: Kanban, level: Int): List<Stage>
#Modifying
#Transactional
#Query("UPDATE Stage s SET s.level = s.level + 1 WHERE s.kanban = :kanban AND s.level >= :level")
fun incrementLevelForKanbanStagesWhereLevelIsGreaterThan(kanban: Kanban, level: Int)
}
the incrementLevelForKanbanStagesWhereLevelIsGreaterThan method fails as the database have a unique constraint to level and kanban_id with the following error:
Caused by: org.postgresql.util.PSQLException: ERROR: duplicate key value violates unique constraint "stages_kanban_id_level_key"
Detalhe: Key (kanban_id, level)=(337, 1) already exists.
this is obviously happening because it is trying to update level 0 to level 1 before updating level 1 to level 2 and so I have tried:
#Transactional
#Query("UPDATE Stage s SET s.level = s.level + 1 WHERE s.kanban = :kanban AND s.level >= :level ORDER BY s.level DESC")
fun incrementLevelForKanbanStagesWhereLevelIsGreaterThan(kanban: Kanban, level: Int)
which does not compile,
#Service
#Transactional
class StageCrudService: CrudService<Stage, Int, StageRepository, StageValidationService>() {
#Throws(ValidationException::class)
override fun create(model: Stage): Stage {
prepareToCreate(model)
validationService.canSave(model)
incrementKanbanStageLevels(model)
return repository.save(model)
}
private fun prepareToCreate(model: Stage) {
val now = LocalDateTime.now()
val closed = model.closed ?: false
model.closed = closed
model.createdAt = now
model.updatedAt = now
model.level = model.level ?: 0
}
private fun incrementKanbanStageLevels(model: Stage) {
val level = model.level ?: 0
val stages = repository.findAllByKanbanAndLevelGreaterThanEqualOrderByLevelDesc(model.kanban!!, level)
stages.forEach { stage ->
stage.level = stage.level?.plus(1)
}
repository.saveAll(stages)
repository.flush()
}
}
and
private fun incrementKanbanStageLevels(model: Stage) {
val level = model.level ?: 0
val stages = repository.findAllByKanbanAndLevelGreaterThanEqualOrderByLevelDesc(model.kanban!!, level)
stages.forEach { stage ->
stage.level = stage.level?.plus(1)
repository.save(stage)
}
repository.flush()
}
but both fails the same way as the query. Now the question is:
Is there a better way to manage the update order for this kind of situation instead of doing:
private fun incrementKanbanStageLevels(model: Stage) {
val level = model.level ?: 0
val stages = repository.findAllByKanbanAndLevelGreaterThanEqualOrderByLevelDesc(model.kanban!!, level)
stages.forEach { stage ->
stage.level = stage.level?.plus(1)
repository.saveAndFlush(stage)
}
}
It seems to me that you are possibly trying to implement something that can be managed for you via the JPA #OrderColumn annotation:
https://docs.oracle.com/javaee/7/api/javax/persistence/OrderColumn.html
Specifies a column that is used to maintain the persistent order of a
list. The persistence provider is responsible for maintaining the
order upon retrieval and in the database. The persistence provider is
responsible for updating the ordering upon flushing to the database to
reflect any insertion, deletion, or reordering affecting the list.
To use this you would need to make the relationship bi-directional and the level should be maintained by your JPA provider as items are added to and removed from the list
#Entity
#Table(name = "kanbans")
data class Kanban (
.....
#get:NotNull
#get:NotBlank
#OrderColumn(name = "level")
#OneToMany(fetch = FetchType.LAZY, mappedBy = "kanban")
var stage: List<Stage> = ArrayList()
.....
}
So you can then remove and add items (at any position) and the sequence will be maintained for you.

Resources