I am using Spring Data MongoDB with Kotlin Coroutines to create a non-blocking server.
I have a document Deck:
#Document
data class Deck(
#Id val id: ObjectId = ObjectId.get(),
#DocumentReference(lazy = true) val author: User,
val title: String,
val desc: String,
#DocumentReference(lazy = true) val cards : Set<Card> = emptySet(),
#CreatedDate val createdAt: Instant = Instant.now(),
#LastModifiedDate val lastModifiedAt: Instant = Instant.now()
)
Deck has a reference to a User type in the author field:
#Document
data class User(
#Id val id: ObjectId = ObjectId.get(),
val name: String,
#Indexed(unique = true, name = "username_index") val username: String,
val password: String,
#CreatedDate val createdAt: Instant = Instant.now(),
#LastModifiedDate val lastModifiedAt: Instant = Instant.now(),
#DocumentReference(lazy = true) val decks: Set<Deck> = emptySet()
)
Here's my DeckRepository interface:
interface DeckRepository: CoroutineSortingRepository<Deck, String> {
}
The Deck documents are getting saved properly in the database with the author's ObjectId.
But when I try to fetch the deck with id, the repository method gives an error. It's not able to populate the author's reference.
override suspend fun getDeck(deckId: String): DeckResponseDTO {
log.info("Getting deck with id $deckId")
return deckRepository.findById(deckId)?.toDto() ?: throw Exception("Could not find deck with id $deckId")
}
Stacktrace snippets:
Failed to instantiate io.gitub.startswithzed.dex.model.Deck using constructor fun <init>(org.bson.types.ObjectId, io.gitub.startswithzed.dex.model.User, kotlin.String, kotlin.String, kotlin.collections.Set<io.gitub.startswithzed.dex.model.Card>, java.time.Instant, java.time.Instant): io.gitub.startswithzed.dex.model.Deck with arguments 63173407712ec0263535829a,null,Test title,Test Desc,null,2022-09-06T11:50:31.370Z,2022-09-06T11:50:31.370Z,16,null
at org.springframework.data.mapping.model.KotlinClassGeneratingEntityInstantiator$DefaultingKotlinClassInstantiatorAdapter.createInstance(KotlinClassGeneratingEntityInstantiator.java:215) ~[spring-data-commons-2.7.2.jar:2.7.2]
Caused by: java.lang.NullPointerException: Parameter specified as non-null is null: method io.gitub.startswithzed.dex.model.Deck.<init>, parameter author
at io.gitub.startswithzed.dex.model.Deck.<init>(Deck.kt) ~[main/:na]
at io.gitub.startswithzed.dex.model.Deck.<init>(Deck.kt:14) ~[main/:na]
I even verified the data by querying using lookup in the console. Somehow Spring Data is not able to query properly.
Thanks for the help!
Related
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")
Suppose I have signUp method inside rest controller class looks like this.
#PostMapping("/signup")
fun authenticateSignUp(#RequestBody command: RegisterUserCommand): CompletableFuture<String> {
return commandGateway.send<String>(command)
}
So it requires request body which is RegisterUserCommand.
data class RegisterUserCommand(
val userId: String,
val balance: BigDecimal,
val username: String,
private val email: String,
private val password: String
)
I want to ignore some fields like userId, balance so I can generate it later inside controller like this
#PostMapping("/signup")
fun authenticateSignUp(#RequestBody request: RegisterUserCommand): CompletableFuture<String> {
val command = request.copy(userId = ObjectId.get().toHexString(), balance = BigDecimal.ZERO)
return commandGateway.send<String>(command)
}
Are there any annotation to ignore this field so it won't return bad request even though I didn't put userId, balance within request body
As correctly pointed out by #flaxel, you can use ? from Kotlin and I believe add #JvmOverloads constructor to it as well.
But, in my opinion, using DTO is the best way to deal with that for sure.
The input of your Controller should not be the Command but just a DTO with the fields you are interested in. How you build your command with enhanced values should not be affected by it.
In this case, you would have something like this (also added #TargetAggregateIdentifier because probably you missed it):
data class RegisterUserCommand(
#TargetAggregateIdentifier
val userId: String,
val balance: BigDecimal,
val username: String,
private val email: String,
private val password: String
)
...
data class RegisterUserDto(
val username: String,
private val email: String,
private val password: String
)
...
#PostMapping("/signup")
fun authenticateSignUp(#RequestBody request: RegisterUserDto): CompletableFuture<String> {
val command = new RegisterUserCommand // build the command the way you want it
return commandGateway.send<String>(command)
}
I guess you can mark the variable as nullable with the ? operator. But I would suggest to use DTOs.
data class RegisterUserDto(
val username: String,
private val email: String,
private val password: String
)
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.
I have a collection, with documents having a field named _id of type String, not generated manually.
I have been trying to get a document using its id.
val criteria = Criteria.where("_id").`is`("a2z3e44R")
val document = mongoTemplate.findOne(Query.query(criteria), MyDocument::class.java) // returns null
val criteria = Criteria.where("_id").`is`(ObjectId("a2z3e44R"))
val document = mongoTemplate.findOne(Query.query(criteria), MyDocument::class.java) // returns null
val document = mongoTemplate.findById("a2z3e44R", MyDocument::class.java) // returns null
mongoTemplate.findAll(MyDocument::class.java).first { myDocument ->
myDocument._id == "a2z3e44R"
} // OK...
MyDocument is
data class MyDocument(val _id: String, val name: String)
Trying to find a document by another field works.
An idea of what I could be missing or a workaround?
You should indicate the type of the id like this
public class Article {
#MongoId(value = FieldType.OBJECT_ID)
private String id;
private String title;
private String desc;
}
Try mark _id with annotation #Id. The #Id annotation is used to specify the identifier for Spring.
data class MyDocument(#Id val _id: String, val name: String)
Ypu could define in your repository:
public interface MyDocumentRepository extends MongoRepository<MyDocument, String> {
Pets findBy_id(ObjectId _id);
}
and use it :
myDocumentRepository.findBy_id("a2z3e44R");
for more info see
or
ObjectId objID = new ObjectId("a2z3e44R");
query.addCriteria(Criteria.where("_id").lt(objID));
like this other answer link
I want to save the server’s response in database (class Parent). The json has nested object, which also should be saved in database in new table (class Nested). The problem is what I don’t know how to write class Parent and ParentDao to make it use NestedDao
#Entity
data class Parent(
#PrimaryKey(autoGenerate = true)
var id: Long? = null,
#SerializedName(«nested»)
val homeTeam: Nested,
//other fields
)
#Entity
data class Nested(
#PrimaryKey(autoGenerate = true)
var nestedId: Long? = null,
#SerializedName("name")
val name: String,
//other fields
)
#Dao
interface ParentDao {
#Query("SELECT * FROM parent»)
fun getData(): Single<List<Parent>>
#Insert
fun insert(matches: List<Parent>)
}
This gives me an error: Cannot figure out how to save this field into database. You can consider adding a type converter for it.
So, what should I do to save and query Parent with Nested at once?
I don't know if you've succeed or not, but here is my answer I hope it'll help you.
That's what I used for my project and what is recommended for Room in Android docs
#Entity
data class Parent(
#PrimaryKey(autoGenerate = true)
var id: Long? = null,
#Embedded #ColumnInfo(name= "nested")
val homeTeam: Nested,
//other fields
)
data class Nested(
#PrimaryKey(autoGenerate = true)
var nestedId: Long? = null,
#ColumnInfo(name= "name")
val name: String,
//other fields
)
#Dao
interface ParentDao {
#Query("SELECT * FROM parent»)
fun getData(): Single<List<Parent>>
#Insert
fun insert(matches: List<Parent>)
}