best way to add new properties to a model when using spring boot with mongodb - spring

I'm looking for a better way to change a model with Spring + Mongodb, currently every time a property is added to a model, we have to create a command to be ran in mongosh to add that field to all documents, and then save it so that it can be ran on every environment that the new model is pushed to.
So for example, lets say a Event object has the properties:
#Document
data class Device(
#Id
val id: String? = null,
#Indexed(unique = true)
val name: String,
var location: String,
)
And we want to add a field "date": 2023-02-02T20:10:19.111Z to it. Currently I will have to create a mongosh command to update all events on the collection to add that field, so something like this:
db.device.updateMany({}, {$set: {'date': new Date().toISOString()}})
We then save this, and remember to run it every single time we merge to a upstream branch.
Is there a better way to define a new model with the date, so that it can create the field automatically?

I would add the new property with a default value, but everyone usecase/trade-offs is different.
#Document
data class Device(
#Id
val id: String? = null,
#Indexed(unique = true)
val name: String,
var location: String,
val newProperty:String? = null
)
This will allow you to get values don't exist in the database as null

Related

How to use BeanUtils.copyProperties on data classes?

I recently discovered the method copyProperties from BeanUBeanUtilstils class and I was wondering how can I copy the properties of a class into a new empty class? for example:
data class User(val name: String, age: Int)
data class ConvertedUser(val name: String, age: Int, address: String)
I already have a class User with the properties filled and I want to create a new class ConvertedUser using the copyProperties but I can't instantiate a new ConvertedUser without passing the parameters to the constructor.
import org.springframework.beans.BeanUtils
val user = User("name", 1)
val cUser = ConvertedUser() // No value passed for parameter name, age and address.
BeanUtils.copyProperties(user, cUser)
Is it possible to copy properties from an existing class to a new "empty" class?

Spring/Kotlin: How do I make a missing JSON field deserialize to the default value given by my entity class, and not to null?

I have an entity, defined using a data class:
#Entity
data class BlogPost(
(...)
val title: String,
(...)
val slug: String = title.toSlug(),
)
As per this answer, I expected that a JSON object without a slug field would deserialize to a BlogPost whose slug was created using my .toSlug() extension method – but, to my surprise, the deserialized .slug ends up being null even though I thought it wasn't nullable (it's a String, not a String?).
I've tried using #JsonInclude(Include.NON_NULL), but I suspect this is the wrong tactic, and it only seems to affect serialization; the missing JSON value is still deserialized to null rather than my default.
I'm on Spring Boot 2.4.3.

I get inconsistent outcomes in Spring Data Neo4j when mapping both ends of a relationship and saving it

I have a User node with FRIEND_REQUEST relationships mapped to a sentFriendRequestList list and to a receivedFriendRequestList list like below:
#Node
data class User(
#Id
#GeneratedValue(UUIDStringGenerator::class)
var userId: String?,
#Relationship(type = "FRIEND_REQUEST", direction = Relationship.Direction.OUTGOING)
var sentFriendRequestList: MutableList<FriendRequest> = mutableListOf(),
#Relationship(type = "FRIEND_REQUEST", direction = Relationship.Direction.INCOMING)
var receivedFriendRequestList: MutableList<FriendRequest> = mutableListOf(),
var email: String
)
The FriendRequest class:
#RelationshipProperties
data class FriendRequest(
#Id
#GeneratedValue
var friendRequestId: Long?,
/**
* Represents the receiver in an OUTGOING relationship and the sender in an INCOMING relationship.
*/
#TargetNode
var friendRequestOtherNode: User
){
constructor(friendRequestOtherNode: User) : this(null, friendRequestOtherNode)
}
When saving multiple friend requests, on some occasions all previously created relationships disappear from the given nodes and only the newly created relationship appears.
I save like this:
fun saveFriendRequest(sender: User, receiver: User) {
val sentFriendRequest = FriendRequest(receiver)
val receivedFriendRequest = FriendRequest(sender)
sender.sentFriendRequestList.add(sentFriendRequest)
receiver.receivedFriendRequestList.add(receivedFriendRequest)
userRepository.save(sender)
userRepository.save(receiver)
}
I don't understand what the problem is, especially since sometimes it runs without failure.
I created a small test project which is available on my GitHub: link. It contains the data structure and a test class that can be run instantly. The tests show the same problem, after multiple runs it can either fail or be successful.
I am a little bit unsure about the right query, but your custom Cypher statement does not return all friends and relationships for deeper links.
Changing it to:
MATCH (user:User{email: \$email})
OPTIONAL MATCH path=(user)-[:FRIEND_REQUEST*]-(friend:User)
WITH user, friend, relationships(path) as friend_requests
RETURN user, collect(friend_requests), collect(distinct(friend))
solved this for me (or the ten test runs were just randomly green).
Another solution would be, to avoid the custom query, define findByEmail(email: String): User and let Spring Data Neo4j create the query.
The problem that occurs is that
First operation:
frodo -> sam
sam -> frodo
Second operation:
frodo -> bilbo
bilbo -> frodo
results for Sam in something like sam-frodo-bilbo.
When you load Frodo or Bilbo the relationships are not completely hydrated to Sam.
The moment you save Bilbo (or Frodo), SDN will through all relationship and eventually not find the Sam relation. Because it is empty, SDN will remove the relationship on save.
Another problem I have seen in your code:
You should definitely annotate the manual defined constructor with #PersistenceConstructor because Kotlin creates an invisible copy constructor and Spring Data in general does not know which to choose. So you might randomly run into the copy constructor.

How to gracefully transform entity into DTO in Kotlin?

I am working on Kotlin + SpringBoot web service, in which I want to transform DTOs into entities in the most convenient way.
Entities:
#Entity
data class Shop(
#Id
#GeneratedValue
val id: Long,
val name: String
#OneToOne
val owner: User,
...
)
#Entity
data class User(
#Id
#GeneratedValue
val id: Long,
val name: String,
...
)
DTO:
data class ShopDTO(
val id: Long,
val name: String,
val ownerId: Long,
val ownerName: String,
...
)
So when someone wants to create a new Shop, my service gets a ShopDTO(name, ownerId) as request body, then I need to transform it into Shop object to be able to save it to the DB. Now here is how my mapper function looks like:
fun fromDTO(source: ShopDTO) = Shop(
id = source.id,
name = source.name,
owner = ???,
...
)
To be able to store a Shop with an owner I only need an id. It would be enough to create a new User with the given ownerId.
To achive this I tried these solutions:
Add default value to the fields in the User class.
Make the fields nullable.
Add a secondary constructor. This also needs default values.
Use some reflection magic to create an empty object and then set the id.
Call a findById method on the UserRepository with the given id.
I want to keep the non-null, immutable fields of my entities and do not want to use reflection. Also do not want to run an unnecessary select DB query just to get back the user by the id.
Could you please suggest me other options? How would you handle this situation? Is there any good mapper framework in Kotlin which can solve this problem?
Firstly, your question says you want to do entity -> DTO, but actually you want to do DTO -> entity, so you should clear that up.
Secondly, you are getting the shop name and owner Id in the ShopDTO. But you are assigning the owner Id to the shop Id in the your fromDTO(source: ShopDTO) function. Changing it up would be sufficient.
fun fromDTO(source: ShopDTO) = Shop(
name = source.name,
owner = ownerRepo.findById(source.ownerId)
)
Obviously, if you're using JPA, then you have to make a DB call to get the owner first. If your business logic doesn't ensure that a User with that Id exists, then you could write a method like this to make a user.
fun getOrCreateUser(ownerId: Long) =
ownerRepo.findUserById(ownerId) ?: User(
id = ownerId,
name = "Some random DefaultName"
).run(ownerRepo::save)
This would get a User by the Id if it exists, or create a new user with some generic name.
Do let me know if this solves your issue!

How do I encode a UUID to make it JSON serializable

I am using UUID instead of the default Django incremental IDs. However, now I get the following error:
file "/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/json/encoder.py",
line 184, in default
raise TypeError(repr(o) + " is not JSON serializable") TypeError: UUID('4fd5a26b452b4f62991d76488c71a554') is not JSON serializable
Here is my serializer file:
class ApplicationSerializer(serializers.ModelSerializer):
class Meta:
model = Application
fields = ("id", "created_at", "merchant_uri", "api_key",
"status", 'owner_email', 'business_type', 'full_name',
'owner_phone_number', 'marketplace_name', 'domain_url',
'support_email', 'support_phone_number', 'postal_code',
'street_address', 'current_processor',
'current_monthly_volume')
This usually means you need to force your UUID to be serialized as a string, which can be done with the CharField. Some UUID field implementations for Django will do this by default, but it appears as though the one you are using will return the raw UUID object. By setting the field to a CharField, this will force it to be converted to a string.
class ApplicationSerializer(serializers.ModelSerializer):
id = serializers.CharField(read_only=True)
class Meta:
model = Application
fields = ("id", "created_at", "merchant_uri", "api_key",
"status", 'owner_email', 'business_type', 'full_name',
'owner_phone_number', 'marketplace_name', 'domain_url',
'support_email', 'support_phone_number', 'postal_code',
'street_address', 'current_processor',
'current_monthly_volume')
This will convert it to a string manually and will give you the output you are expecting.
You can use django-uuidfield, it serializes automatically.

Resources