Springboot Mongo reactive repository unable to update nested list - spring

I wanted to update a nested list but I experience a strange behavior where I have to call method twice to get it done...
Here is my POJO:
#Document(collection = "company")
data class Company (
val id: ObjectId,
#Indexed(unique=true)
val name: String,
val customers: MutableList<Customer> = mutableListOf()
//other fields
)
Below is my function from custom repository to do the job which I based on this tutorial
override fun addCustomer(customer: Customer): Mono<Company> {
val query = Query(Criteria.where("employees.keycloakId").`is`(customer.createdBy))
val update = Update().addToSet("customers", customer)
val upsertOption = FindAndModifyOptions.options().upsert(true)
//if I uncomment below this will work...
//mongoTemplate.findAndModify(query, update, upsertOption, Company::class.java).block()
return mongoTemplate.findAndModify(query, update, upsertOption, Company::class.java)
}
In order to actually add this customer I have to either uncomment the block call above or call the method two times in the debugger while running integration tests which is quite confusing to me
Here is the failing test
#Test
fun addCustomer() {
//given
val company = fixture.company
val initialCustomerSize = company.customers.size
companyRepository.save(company).block()
val customerToAdd = CustomerReference(id = ObjectId.get(),
keycloakId = "dummy",
username = "customerName",
email = "email",
createdBy = company.employees[0].keycloakId)
//when, then
StepVerifier.create(companyCustomRepositoryImpl.addCustomer(customerToAdd))
.assertNext { updatedCompany -> assertThat(updatedCompany.customers).hasSize(initialCustomerSize + 1) }
.verifyComplete()
}
java.lang.AssertionError:
Expected size:<3> but was:<2> in:

I found out the issue.
By default mongo returns entity with state of before update. To override it I had to add:
val upsertOption = FindAndModifyOptions.options()
.returnNew(true)
.upsert(true)

Related

How to treat nested entity add/update/delete operations in Hibernate?

I once again joined a project which uses Hibernate (Spring/Hibernate/Kotlin to be exact) and have read through a number of Vlad Mihalcea wonderful articles to refresh my knowledges about this ORM (this article is of my current interest).
What I'm trying to understand is how should I treat add/update/delete operations for nested entities (bidirectional #OneToMany). Here is what I don't understand.
Say we have a Post entity:
#Entity(name = "Post")
#Table(name = "post")
class Post(
#Id
#GeneratedValue
var id: Long? = null,
val title: String,
#OneToMany(mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true)
private val comments: MutableList<PostComment> = MutableList<PostComment>()
) {
fun addComment(comment: PostComment) {
comments.add(comment)
comment.post = this
}
fun removeComment(comment: PostComment) {
comments.remove(comment)
comment.post = null
}
}
And a PostComment entity:
#Entity(name = "PostComment")
#Table(name = "post_comment")
class PostComment(
#Id
#GeneratedValue
var id: Long? = null,
val review: String,
#ManyToOne(fetch = FetchType.LAZY)
var post: Post
) {
override fun equals(o: Any?): Boolean {
if (this === o) return true
if (o !is PostComment) false
return id != null && id == o.id
}
override fun hashCode(): Int {
return javaClass.hashCode()
}
}
All in all everything is good, but here is the couple of things I don't know how to cover:
In fact Post class won't compile since I set post field of PostComment to null while it is not nullable. What is a good practice to handle it? Should I make all relations nullable in kotlin just because hibernate require it to be so and it is in contradiction with business logic?
It is more or less clear how to add and delete nested entities, though what should we do if we need to update already existing nested entity. Let's imagine we have a Post(id=1, title="lovely post", comments=[PostComment(id=15, review="good", post=this)] and we get a update action with the following PostDto(id=1, title="not that nice post", comments=[PostComment(id=15, review="bad", post=this)]. As you can see we need to update title for Post and review for PostComment. If we take a look at the Vlad's article I linked above we do not see any update methods. I think it was just ommited since it is not related to article topic.
But I wonder what is the good practice to handle such an update? Something like these two approaches comes to my mind but I'm not sure if these are the best things to do:
#Entity(name = "Post")
#Table(name = "post")
class Post(
//fields...
) {
fun addComment(comment: PostComment) {
comments.add(comment)
comment.post = this
}
fun removeComment(comment: PostComment) {
comments.remove(comment)
comment.post = null
}
// not effective, since issue delete/insert queries, but clean
fun updateComment(comment: PostComment) {
val commentId = comment.id!!
comments.removeIf { it.id == commentId }
comment.team = this
}
// effective, since issue only update query, but dirty as hell
fun updateComment(commentId: Long, review: String) {
val comment = comments.find { it.id == commentId }!!
comment.review = review
}
}
Not actual anymore. Refer good explanation by #Chris in a comment section.
Imagine we need an endpoint to update a comment only. What
is the best way to organise our code base for such a scenario?
Should we always update it like this (always fetch old post, looks
inefficient) or is there any better/efficient approach?
#Transactional
fun reassignComment(newPostId: Long, commentDto: CommentDto) {
val comment = commentRepo.findByIdOrNull(commentDto.id)!!
val oldPost = comment.post
val newPost = postRepo.findByIdOrNull(newPostId)!!
oldPost.removeComment(comment)
newPost.addComment(comment)
}
Thanks anyone for your time and input!

SonarCloud coverage stating that class property with JsonProperty annotation not covered by tests

I have a Kotlin project with Spring, and I created a class that looks like the following:
#JsonIgnoreProperties(ignoreUnknown = true)
data class Response(
val id: String,
#JsonProperty("quantity_of_days")
val quantityOfDays: Int,
)
And my SonarCloud reports state that the quantityOfDays line is not covered by tests:
This line is accessed multiple times inside my tests, and I even created one specifically to instantiate an object of that class. However, this line is still marked as not covered.
I wonder if it has something to do with the annotation, and if so, how do I ignore or force this line to be covered?
Ok so, it was necessary to write some very specific tests to cover that:
class ResponseTest {
#Test
fun `create response from json with underline attribute`() {
val id = "123"
val quantityOfDays = 1
val response = Response(id, quantityOfDays)
val value = Mapper.objectMapper.readValue(
"""
{
"id": "$id",
"quantity_of_days": $quantityOfDays
}
""".trimIndent(), Response::class.java)
assertThat(value).isEqualTo(response)
}
#Test
fun `create response from json with camel case attribute`() {
val response = ResponseBuilder().build()
val json = Mapper.objectMapper.writeValueAsString(response)
val value = Mapper.objectMapper.readValue(json, Response::class.java)
assertThat(value).isEqualTo(response)
}
}
I am not sure if that is the best solution, maybe there is a way to make the coverage ignore that in specific, but I could not find it. But it works.

Use join fetch by default with Spring Data

I have a simple schema "Posts have messages that have users" and I want get messages with User data.
#Entity
class User {
#Id
var id: Int = 0
var name: String = ""
var usenrame: String = ""
}
#Entity
class Messages {
#Id
var id: Int = 0
var text: String = ""
#ManyToOne()
var user = User()
}
class Post {
var id: Int = 0
#OneToMany()
#JoinColum()
var messsages: List<Messages> = ArrayList()
}
This works, but, Spring Data / Hibernate, instead of create a single query with join to get message and user, use two queries
I can use a custom repository method to get directly a message and it works fine
#Query("SELECT m FROM Message m LEFT JOIN FECTH m.user WHERE id =:id")
fun findMessageById(id: Int)
But... I don't get any way to get a Post with all messages and users...
I understand that each Post need a query to get all messages, but, I want that query get too user and not get another query for each message to get the user...
Any ideas?

Swagger 2 UI How to show models that are not explicitly returned by RestController

I'm having following issue, on swagger under Models, i see just abstract Base class that is extended by 3 other classes. My current end point returns Base type of class, because i can have 3 different types returned on one end point.
So basically i have something like this
#MappedSuperclass
#ApiModel(description = "Base Details.")
abstract class BaseClass(
open var id: String? = null,
var prop1: String? = null,
var prop2: String? = null,
var prop3: String? = null,
var prop4: String? = null
)
#ApiModel(description = "Some Specific Details that contains all base properties.")
data class AnotherClass(
val prop4: String,
val prop5: String,
val prop6: Set<Amount>,
val prop7: Set<Amount>,
val prop8: String
) : BaseClass()
#ApiModel(description = "Some more Specific Details that contains all base properties.")
data class OneMoreClass(
val prop4: String,
val prop5: String
) : BaseClass()
And in RestController i have this
#GetMapping
#ApiOperation(value = "End point description", notes = "Notes notes notes.")
fun getSomethingFromDatabase(): List<BaseClass> {
return someService.getData();
}
So issue that i have is on swagger UI, under Models section i see just BaseClass and no other classes at all...
I tried this, because somewhere i seen this example:
#ApiModel(description = "Base Details.", subTypes = {AnotherClass.class})
BaseClass
but this way i have "kotlin" issue, that is saying "name is missing", also i can not do AnotherClass::class...
You will have to add those in the config as below:
return new Docket(DocumentationType.SWAGGER_2)
.additionalModels(typeResolver.resolve(AnotherClass.class), typeResolver.resolve(OneMoreClass.class))
.....
subTypes is still not completely supported in Swagger 2, still has an open ticket
For your Kotlin config, this is how it should look like:
subTypes = [AnotherClass::class, OneMoreClass::class]
I have just added a sample Kotlin controller for you to refer in my github project. Look for AnimalController.kt & SwaggerConfig for required setup.

#Param not working in Spring Data JPA

I'm setting up a Spring Data JPA Repo to work with sequences in a postgresql database. I was assuming that this would be pretty simple:
#Query(nativeQuery = true, value = "CREATE SEQUENCE IF NOT EXISTS ':seq_name' START WITH :startAt")
fun createSequence(#Param("seq_name") seq_name: String, #Param("startAt") startAt: Long = 0)
#Query(nativeQuery = true, value = "SELECT nextval(':seq_name')")
fun nextSerial(#Param("seq_name") seq_name: String) : Long
#Query(nativeQuery = true, value = "DROP SEQUENCE IF EXISTS ':seq_name'")
fun dropSequence(#Param("seq_name") seq_name: String)
#Query(nativeQuery = true, value = "setval(':seq_name', :set_to, false")
fun setSequence(#Param("seq_name") seq_name: String, #Param("set_to") setTo: Long)
But for some reason I get
org.springframework.dao.InvalidDataAccessApiUsageException: Parameter with that name [seq_name] did not exist; whenever I'm trying to call the method. Any idea why this might happen?
Ok, based on the answer from #StanislavL and after some debugging around I have a working solution now. As #posz pointed out I cannot bind identifiers which means I have to hard code the queries. I moved the code from a JPA interface to an implemented service which is not as nice but works.
#Service
open class SequenceService (val entityManager: EntityManager){
#Transactional
fun createSequence(seq_name: String, startAt: Long = 0) {
val query = entityManager.createNativeQuery("CREATE SEQUENCE IF NOT EXISTS ${seq_name} START ${startAt}")
with(query){
executeUpdate()
}
}
#Transactional
fun nextSerial(seq_name: String) : Long {
val query = entityManager.createNativeQuery("SELECT nextval(:seq_name)")
with(query){
setParameter("seq_name", seq_name)
val result = singleResult as BigInteger
return result.toLong()
}
}
#Transactional
fun dropSequence(seq_name: String) {
val query = entityManager.createNativeQuery("DROP SEQUENCE IF EXISTS ${seq_name}")
with(query){
executeUpdate()
}
}
#Transactional
fun setSequence(seq_name: String, setTo: Long){
val query = entityManager.createNativeQuery("SELECT setval(:seq_name, :set_to, false)")
with(query){
setParameter("seq_name", seq_name)
setParameter("set_to", setTo)
singleResult
}
}
}
I hope this is helpful for the next person trying to directly work with sequences when using #SequenceGenerator is not an option.

Resources