Neo4jRepository saves empty node from Kotlin data class - spring-boot

I have a Kotlin data class node for Neo4j nodes:
#NodeEntity
data class MyNode (
#Id #GeneratedValue var dbId: Long? = null,
#Index(unique = true) val name: String,
val description: String
)
and a Spring repository:
interface MyNodesRepository : Neo4jRepository<MyNode, Long>
Then, when I save a node into the DB via this repository it is empty, without any properties:
val node = MyNode(null, "name 1", "lorem ipsum")
myNodesRepository.save(node)
after the save(node) call, the node.dbId is set to the Neo4j's internal id i.e. it is null before save() and has a value afterwards. I can also see the node in the Neo4j browser, but it does not have name and description properties.
After that, when I try to load all nodes the call crashes with InvalidDataAccessApiUsageException because it cannot deserialize/map the nodes with null/missing name and description:
val allNodes = myNodesRepository.findAll()
If I add a custom save method to my repository, where I manually create the node with CQL query, then everything works.
interface MyNodesRepository : Neo4jRepository<MyNode, Long> {
#Query(
"MERGE (mn:MyNode {name:{name}})\n" +
"ON CREATE SET m += {description:{description}}"
)
fun customSave(#Param("name") name: String, #Param("description") description: String)
}
Now the findAll() loads my newly created and/or updated nodes.
I am using org.springframework.boot:spring-boot-starter-data-neo4j:2.1.6.RELEASE and this is inside a Spring Boot CLI application so no web server and RestControllers.
What am I doing wrong?
EDIT: This is now solved in Neo4j OGM 3.1.13

EDIT: This is now solved in Neo4j OGM 3.1.13 and the workaround below is not needed anymore.
ORIGINAL ANSWER
After few days of debugging it looks like there is a bug in Neo4j OGM for Kotlin data classes where it does not save val properties -- both in data classes and normal classes. So change from val properties:
#NodeEntity
data class MyNode (
#Id #GeneratedValue var dbId: Long? = null,
#Index(unique = true) val name: String,
val description: String
)
to all var properties:
#NodeEntity
data class MyNode (
#Id #GeneratedValue var dbId: Long? = null,
#Index(unique = true) var name: String,
var description: String
)
works. Now both saving and loading works, but it is not idiomatic Kotlin.
So at least we have a workaround.

Related

Neo4J (spring boot) with sealed classes in kotlin

I'm trying to create a somewhat generic implementation using kotlin and neo4j. My idea right now is that I want to have a GeoNode that can point to any kind of GeoJson feature (e.g. "Feature" or "FeatureCollection")
I tried to do this using kotlins sealed classes, e.g.
#JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
include = JsonTypeInfo.As.PROPERTY,
property = "type"
)
#JsonSubTypes(
JsonSubTypes.Type(value = GeoJsonFeature::class, name = "GeoJsonFeature")
)
#Node
sealed class FeatureContract(
#GeneratedValue #Id var id: Long? = null,
#Version var version: Long? = null
) {
companion object {
#JsonCreator
#JvmStatic
private fun creator(name: String): FeatureContract? {
return FeatureContract::class.sealedSubclasses.firstOrNull {
it.simpleName == name
}?.objectInstance
}
}
}
data class GeoJsonFeature(
val geometry: GeometryContract,
) : FeatureContract()
data class GeoJsonFeatureCollection(
val features: List<GeoJsonFeature>,
) : FeatureContract()
// and the "GeoNode" that holds this
#Node
data class GeoNode(
#Id val id: String,
#Version var version: Long? = null
#Relationship(type = "feature") var feature: FeatureContract?, // Should be either a featureCollection or a feature,
)
The idea is that I can have a point on a map that points to any kind of GeoJson.
It seems I am successful in serializing this and getting it into the Neo4J-db on write, however, on reading I get
Failed to instantiate [FeatureContract]: Is it an abstract class?; nested exception is java.lang.InstantiationException
The Jackson annotations are there cause I hoped they would help me (that Neo4J-OGM was using it under the hood) but it doesn't seem to have done the trick. I've read about Neo4JEntityConverters but I haven't understood how one can to this for a full object like this. Is there any good way to use sealed classes in kotlin with the neo4j-OGM for both serialization and deserialization?
Using spring boot 2.4.4 and spring-boot-starter-data-neo4j

Spring Data JDBC Kotlin NoSuchMethod error: Dialect.getIdGeneration()

I'm writing a very simple Spring Data JDBC repository in Kotlin (using Postgres as the database):
data class Label(
#Id
#GeneratedValue
#Column( columnDefinition = "uuid", updatable = false )
val id: UUID,
val name: String
)
#Repository
interface LabelRepository: CrudRepository<Label, UUID> {}
When I do repository save:
val l = Label(id = UUID.randomUUID(), name = "name")
labelRepo.save(l)
It works fine. But since id is not null Spring Data JDBC will always treat it as an "update" to an existing label entity instead of creating a new one with generated ID.
So I changed id: UUID to id: UUID? And having val l = Label(id = null, name = "name")
But call the same save() method gives me:
java.lang.NoSuchMethodError: 'org.springframework.data.relational.core.dialect.IdGeneration org.springframework.data.relational.core.dialect.Dialect.getIdGeneration()'
I have tried a solution here: https://jivimberg.io/blog/2018/11/05/using-uuid-on-spring-data-jpa-entities/
But it didn't work, still gives me the same error
Wondering what's the cause of this and why this error pops up only when I change UUID to UUID??
nvm, turns out I have to use the implementation("org.springframework.boot:spring-boot-starter-data-jdbc") dependency instead of implementation("org.springframework.data:spring-boot-starter-data-jdbc:2.1.3")

Spring Data JDBC - Kotlin support - Required property not found for class

I'm trying to use Spring Data JDBC with Kotlin data-classes, and after adding #Transient property to primary constructor I received error on simple findById call:
java.lang.IllegalStateException: Required property transient not found for class mitasov.test_spring_data_with_kotlin.Entity!
my entity class looks like below:
data class Entity(
#Id
var id: String,
var entityName: String,
#Transient
var transient: List<TransientEntity>? = mutableListOf(),
)
After reading that issue I've tried to make #PersistenseConstructor without #Transient field:
data class Entity(
#Id
var id: String,
var entityName: String,
#Transient
var transient: List<TransientEntity>? = mutableListOf(),
) {
#PersistenceConstructor
constructor(
id: String,
entityName: String,
) : this(id, entityName, mutableListOf())
}
But this didn't help me and I'm still getting that error.
How can I solve this problem?
It's turned out that my second attempt IS the solution.
The trick was in my Run/Debug configuration for tests.
In IDEA Preferences I have checked Preferences | Build, Execution, Deployment | Build Tools | Maven | Runner — Delegate IDE build/run actions to Maven checkbox, and this means that I need to manually recompile my project before running tests.
Solution
So, this is it, the solution for error
java.lang.IllegalStateException: Required property transient not found for class mitasov.test_spring_data_with_kotlin.Entity!
is making #PersistenseConstructor without #Transient fields:
data class Entity(
#Id
var id: String,
var entityName: String,
#Transient
var transient: List<TransientEntity>? = mutableListOf(),
) {
#PersistenceConstructor
constructor(
id: String,
entityName: String,
) : this(id, entityName, mutableListOf())
}

How to tell Spring Data MongoDB to store nested fields of a document that are also documents in their own collection?

I have two collections called persons and addresses. The idea is to have person hold an address in the field address. I use Spring Data MongoDB to persist those mentioned documents.
My usual way of crafting the "relation" between Person > Address was to store the ID of the address and give it to the person object. Later when I find() a person I resolve the address object by it's id and voila I have my person + address.
However I find this somewhat every cumbersome since in my code I just want to add the Address object as whole and not only it's ID so I can work with it while also saving it to the repository at any point of time.
I therefore started a little unit test to see how Spring Data MongoDB saves the Address object if it's just a field of Person and is not saved by it's own Repository.
This is what I came up with:
import org.springframework.data.mongodb.core.mapping.Document
import org.springframework.data.mongodb.repository.MongoRepository
import org.springframework.stereotype.Repository
#Document("person")
data class Person(
val id: String,
val name: String,
val age: Int,
var address: Address
)
#Document("addresses")
data class Address(
val id: String,
val street: String?,
val number: Int?
)
#Repository
interface PersonRepository : MongoRepository<Person, String>
#Repository
interface AddressRepository : MongoRepository<Address, String>
And this is the unit test - that fails with the last steps as I was expecting:
internal class FooTest #Autowired constructor(
private val personRepository: PersonRepository,
private val addressRepository: AddressRepository
) {
#Test
fun `some experiment`() {
val testPerson = Person("001", "Peter", 25, Address("011","Lumberbumber", 12))
personRepository.save(testPerson)
val person = personRepository.findAll()[0]
assertThat(person).isNotNull
assertThat(person.address).isNotNull
assertThat(person.address.street).isEqualTo("Lumberbumber")
assertThat(person.address.number).isEqualTo(12)
// works because address was just copied into the object structure
// of `person` and was not seen as a standalone document
val address = addressRepository.findAll()[0]
assertThat(address.street).isEqualTo("Lumberbumber") // fails
assertThat(address.number).isEqualTo(12) // fails
// As expected `address` was not persisted alongside the `person` document.
}
}
So I thought about using AbstractMongoEventListener<Person> to intercept the saving process and pick the Address object out from Person here and do a addressRepository.save(addressDocument) while putting a lightweight address object (only having the ID) back in the Person document.
The same I'd to in the reverse when doing a find for Person and assembling Person and Address together again.
#Component
class MongoSaveInterceptor(
val addressRepository: AddressRepository
) : AbstractMongoEventListener<Person>() {
override fun onBeforeConvert(event: BeforeConvertEvent<Person>) {
val personToSave = event.source
val extractedAddress = personToSave.address
val idOfAddress = addressRepository.save(extractedAddress).id
personToSave.address = Address(idOfAddress, null, null)
}
override fun onAfterConvert(event: AfterConvertEvent<Person>) {
val person = event.source
val idOfAddress = person.address.id
val foundAddress = addressRepository.findById(idOfAddress)
foundAddress.ifPresent {
person.address = it
}
}
}
It works that way and might be a workaround solution for my requirement.
BUT
I feel that there has to be something like that already working and I might just need to find the proper configuration for that.
That's where I am stuck atm and need some guidance.
Another research showed me it's about #DBRef (https://www.baeldung.com/cascading-with-dbref-and-lifecycle-events-in-spring-data-mongodb) I have to use. This way Spring Data MongoDB stores the embedded document class and resolves it when loading the parent document object from the database.

Spring Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect)

I can't understand, what's wrong with my Service. I receive org.hibernate.StaleObjectStateException trying to run this method:
fun updateNameForPhone(phone: String, name: String): Client {
val res = clientRepository.findByPhone(phone) ?: throw ClientNotFoundException(phone)
res.name = name
return clientRepository.save(res)
}
ClientRepository:
#Repository
interface ClientRepository : JpaRepository<Client, UUID> {
fun findByPhone(phone: String): Client?
}
Client entity:
#Entity
data class Client(
var name: String = "",
var phone: String = "",
#Id #GeneratedValue(strategy = GenerationType.AUTO)
val uuid: UUID = defaultUuid()
)
Exception:
Object of class [com.app.modules.client.domain.Client] with identifier
[12647903-7773-4f07-87a8-e9f86e99aab3]: optimistic locking failed;
nested exception is org.hibernate.StaleObjectStateException: Row was
updated or deleted by another transaction (or unsaved-value mapping
was incorrect) :
[com.app.modules.client.domain.Client#12647903-7773-4f07-87a8-e9f86e99aab3]"
What is the reason?
I'm using Kotlin 1.3.11, Spring Boot 2.1.1, MySql. I don't run it in different threads, just trying with single request.
Well, finally I've found a solution. Better say workaround.
The problem is in the way spring uses UUID as entity identifier.
So there are two workarounds, solving this issue:
first, you can change your id field type to other one, such as Long, for example, if it's possible to you;
or you can add this annotation to your uuid field: #Column(columnDefinition = "BINARY(16)").
The last solution I've found from this question.

Resources