Spring Data R2dbc UUID #Id Persistable - spring-boot

I am not sure whether this is a bug or I am using Persistable incorrectly.
Aim: make the UserId non-null to avoid having to assert non-null when using in the code
User.kt
#Table(value = "main.user")
data class User(
#Id
#Column("id")
var userId: UUID = UUID(0,0),
val username: Username,
val isVerified: Boolean = false,
val updated: LocalDateTime,
val updatedBy: String
) : Persistable<UUID> {
override fun isNew(): Boolean = userId.equals(UUID(0,0))
override fun getId(): UUID = userId
}
UserRepository.kt
#Repository
interface UserModelRepository : CoroutineCrudRepository<User, UUID>
Test.kt
#DataR2dbcTest
internal class Test {
#Autowired
private lateinit var userModelRepository: UserModelRepository
#Test
fun `should save the user model`() = runBlocking {
val userModel = createUserModel()
val actual = UserModelRepository.save(userModel)
actual.userId shouldNotBe UUID(0, 0)
}
}
SQL
CREATE TABLE IF NOT EXISTS main.user
(
id UUID NOT NULL PRIMARY KEY DEFAULT GEN_RANDOM_UUID(),
username CHARACTER VARYING(70) NOT NULL UNIQUE,
is_verified BOOLEAN NOT NULL,
updated TIMESTAMP WITHOUT TIME ZONE NOT NULL,
updated_by CHARACTER VARYING(255) NOT NULL
);
Expected: The test to pass as the Id should be generated by the database
Actual: Id is "00000000-0000-0000-0000-000000000000"

Spring Data uses the same object reference to apply the update of the ID at. Since you set the ID to a final value Spring Data can't apply a new generated ID to it. The ID will also only be generated if the value is null otherwise the value which is already set will be used.
There is a workaround:
#Table("my_user")
data class MyUser(
#Id
#Column("id")
private var _id: UUID? = null,
val name: String = "blubb"
) {
val id: UUID
get() = _id ?: UUID(0, 0)
}
But it is basically bad practice as answered here.

Related

Is there a way to make relation to Set<String> in Room?

Assume tables:
CREATE TABLE users (id INT, name VARCHAR(255))
CREATE TABLE user_roles (user_id INT REFERENCES users(id), role_name VARCHAR(255))
Entity in Room:
#Entity(tableName = "users")
data class User(
#PrimaryKey
val id: Int,
val roles: Set<String> // want it from role_name column
)
Second entity (if needed):
#Entity(tableName = "user_roles")
data class UserRole(
val user_id: Int,
val role_name: String
)
DAO
#Dao
interface UserDao {
#Query("SELECT * FROM users")
fun getAllUsers(): Flow<List<User>>
}
This will not compile because SQLite doesn't support collections. Is there any way to map the role_name from user_roles to roles property of User entity? Can Room do it automatically?
I don't think Room has automatic support for that requirement. Instead you could do a custom query to achieve that.
Refer to this example of how to do one, and adapt according to your requirements.
getAllUsersWithRoles returns flow of list of UserWithRoles. Query joins users and user_roles tables and maps results to UserWithRolesResult class.
fromResult takes list of UserWithRolesResult and maps it to list of UserWithRoles
#Dao
interface UserDao {
#Transaction
#Query("SELECT * FROM users")
fun getAllUsersWithRoles(): Flow<List<UserWithRoles>>
companion object {
private fun fromResult(result: List<UserWithRolesResult>) : List<UserWithRoles> {
val map = result.groupBy { it.userId }.mapValues { it.value.map { it.roleName }.toSet() }
return map.map { UserWithRoles(it.key, it.value) }
}
}
}
data class UserWithRoles(
val userId: Int,
val roles: Set<String>
)
data class UserWithRolesResult(
val userId: Int,
val roleName: String
)
This will not compile because SQLite doesn't support collections.
Room however does.
Consider the following working Demo based upon your code
note the comments and modifications
run on the main thread for brevity
the POJO UserWithRoles is the main inclusion that uses a Set, which is populated/used
:-
#Entity(tableName = "users")
data class User(
#PrimaryKey
val id: Int,
val name: String
/* roles would be effectively trying to store what is stored elsewhere */
//val roles: Set<String> // want it from role_name column !! Wrong place see UserWithRoles
)
#Entity(
tableName = "user_roles",
foreignKeys = [
ForeignKey(
entity = User::class,
parentColumns = ["id"],
childColumns = ["user_id"]
)
]
)
data class UserRole(
#PrimaryKey
/* In Room and Entity MUST have a primary key */
/* if user_id were a primary key then many UserRoles per User would not be allowed as
a primary key is implicitly UNIQUE. Hence dummy_id
*/
val dummy_id: Long?=null,
val user_id: Int,
val role_name: String
)
data class UserWithRoles(
#Embedded
val user: User,
#Relation(entity = UserRole::class, parentColumn = "id", entityColumn = "user_id" )
val roles: Set<UserRole>
)
#Dao
interface UserDao {
#Insert(onConflict = OnConflictStrategy.IGNORE)
fun insert(user: User): Long
#Insert(onConflict = OnConflictStrategy.IGNORE)
fun insert(userRole: UserRole): Long
#Query("SELECT * FROM users")
fun getAllUsers(): /*Flow<*/List<UserWithRoles>/*>*/
}
#Database(entities = [User::class,UserRole::class], exportSchema = false, version = 1)
abstract class TheDatabase: RoomDatabase() {
abstract fun getUserDao(): UserDao
companion object {
private var instance: TheDatabase?=null
fun getInstance(context: Context): TheDatabase {
if (instance==null) {
instance=Room.databaseBuilder(context,TheDatabase::class.java,"the_database.db")
.allowMainThreadQueries() /* for brevity of demo */
.build()
}
return instance as TheDatabase
}
}
}
and some Activity Code to demonstrate:-
class MainActivity : AppCompatActivity() {
lateinit var db: TheDatabase
lateinit var dao: UserDao
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
db = TheDatabase.getInstance(this)
dao = db.getUserDao()
dao.insert(User(10,"Fred"))
dao.insert(User(11,"Mary"))
dao.insert(User(12,"Jane"))
dao.insert(UserRole(user_id = 10, role_name = "Role1"))
dao.insert(UserRole(user_id = 10, role_name = "Role2"))
dao.insert(UserRole(user_id = 10, role_name = "Role3"))
dao.insert(UserRole(user_id = 11, role_name = "Role4"))
for (uwr in dao.getAllUsers()) {
val sb = StringBuilder()
for (r in uwr.roles) {
sb.append("\n\tRole is ").append(r.role_name)
}
Log.d("DBINFO","User is ${uwr.user.name} and has ${uwr.roles.size} roles. They are ${sb}")
}
}
}
When run then the log includes:-
D/DBINFO: User is Fred and has 3 roles. They are
Role is Role3
Role is Role2
Role is Role1
D/DBINFO: User is Mary and has 1 roles. They are
Role is Role4
D/DBINFO: User is Jane and has 0 roles. They are

How can optimised the code for Derived Query Methods in Spring Data JPA Repositories

Entity Class:
#Document(collection = “personErasure")
class PersonErasureModel {
#Id
var id: UUID = UUID.randomUUID()
lateinit var personRequestId: UUID
var fName: String? = null
var validationState: PersonValidationState? = null
var erasureState: ErasureState? = null
}
//1. Enum class
enum class PersonValidationState {
PROCESS,
ELIGIBLE,
INELIGIBLE
}
//2. Enum Class
enum class ErasureState {
IN_PROGRESS,
COMPLETED
}
Repository:
#Repository
interface PersonErasureRepository : ReactiveMongoRepository<PersonErasureModel, UUID> {
fun countByPersonRequestIdAndValidationStateIn(
personRequestId: UUID,
statuses: List<PersonValidationState>
): Mono<Long>
fun countByPersonRequestIdAndErasureState(
personRequestId: UUID,
erasureState: ErasureState
): Mono<Long>
}
Here in PersonErasureRepository both methods are similar. But both PersonValidationState and ErasureState are two different properties in model class.
Can we use a single method which can handle both the use case? Then how can name the Derived query method ?

Spring Boot custom model on top of another

I'm new to Spring Boot and I don't know how to google this, but
I have this situation:
There is table tokens:
id INT
token VARCHAR
type VARCHAR
resource VARCHAR
attributes JSON
created_at DATETIME
And this tokens are multi-purpose: login codes, share codes etc
Login process:
E-mail provided by user
Server sent login code to e-mail
User provides code
Authorization
So in login context, I don't have Token with resource and attributes, but I have LoginCode model which properties code userId (stored as resource) and created_at
So i have model class and repository:
#Entity
class Token(
#Id #GeneratedValue
var id: Long? = null,
#Column(unique = true)
var token: String = generateToken(6),
var type: TokenType,
var resource: String,
#Type(type = "json")
var attributes: Map<String, Any>? = null,
var createdAt: LocalDateTime = LocalDateTime.now(),
)
interface TokenRepository : CrudRepository<Token, Long> {
fun findByTokenAndType(token: String, type: TokenType)
}
But is there a better solution for custom repository than:
interface LoginCodeRepository : TokenRepository {
fun findByCode(code: String): LoginCode {
val token = this.findByTokenAndType(
token = code,
type = TokenType.LOGIN_ATTEMPT,
)
return LoginCode.fromToken(token)
}
fun create(adminId: Long): LoginCode {
val token = this.save(Token(
type = TokenType.LOGIN_ATTEMPT,
resource = adminId.toString(),
))
return LoginCode.fromToken(token)
}
}
Without showing the whole TokenRepository api for public, and not having to call LoginCode.fromToken in each function?
You have entity Token. And you don't need special repository for each purpose using of token. You can make data class (model) for Login. And then you can make in separate utils file extension functions for conversion between Token entityi to different models like Login. I'll show you some example:
#Entity
data class Token(
#Id #GeneratedValue
var id: Long? = null,
#Column(unique = true)
var token: String = generateToken(6),
var type: TokenType,
var resource: String,
#Type(type = "json")
var attributes: Map<String, Any>? = null,
var createdAt: LocalDateTime = LocalDateTime.now()
)
data class LoginCode(
val code: String,
val userId: String,
val createdAt: LocalDataTime
)
fun Token.toLoginCode() = LoginCode(
code = this.token,
userId = this.resource,
createdAt = this.createdAt
)
Then in your service layer, if you will need LoginCode, you can do something like this:
tokenRepository.findByTokenAndType(code, TokenType.LOGIN_ATTEMPT).toLoginCode()
tokenRepository.save(Token(type = TokenType.LOGIN_ATTEMPT, resource = adminId.toString())).toLoginCode()

Not able to delete or find records by id in mongoDB

I am saving customer records in Mongo DB, i am using Angular 6 as a front end.
While saving, i am not sending Id value, so automatically Mongo is creating id and saving records.
I am using MongoRepository in Java for saving. But while doing "deleteById" or "findById", its not able to search or delete those records.
Can you help.
Angular Customer Model
export interface Customer {
id : string,
custId : number,
customerName : string,
email: string,
phone : string,
age: number,
city : string,
state : string,
createdDate : Date
}
User.service.ts
deleteCustomerData(id): Observable<Customer>{
console.log(this.deleteCustomerUrl + id);
return this.http.delete<Customer>(this.deleteCustomerUrl + id);
}
Java Controller
#DeleteMapping("/deleteCustomer/{id}")
public String deleteCustomerById(#PathVariable String id) {
//ObjectId objId = new ObjectId(id);
customerService.deleteCustomerById(id);
return "deleted customer by id"+ id;
}
Java Service
public void deleteCustomerById(String id) {
customerRepository.deleteById(id);
}
Java Model
#Document(collection="Customer")
public class Customer {
#Id
private String Id;
private String customerName;
private int age;
private String city;
private String state;
private int custId;
}
Java Repository
package com.tivo.extract.config.repository;
import org.springframework.data.mongodb.repository.MongoRepository;
import com.tivo.extract.config.model.Customer;
public interface CustomerRepository extends MongoRepository<Customer, String>{
}
The issue is the data type of primary field need to be changed from String to ObjectId.
Mongo db uses ObjecId as primary key type by default.
This is a late response but I solved this issue by changing private String Id to private String id. Seems like it is only a naming problem.
The simple solution you have to make the field (ex. Email Id, name,last name) which you want to use as a key annote it with #Id annotation in the class

Spring Data MongoDB #Indexed(unique = true) in inner field

For example, there are 2 classes:
#Document
public class WordSet {
#Id
private ObjectId id;
private Language language;
private ObjectId languageId;
}
#Document
public class Language {
#Id
private ObjectId id;
#Index(unique = true)
private String languageName;
}
Languages are stored in separate collection and WordSets are stored in another collection. BUT WordSet is stored only with languageId (language is always null when I save WordSet). language field in WordSet is needed, but it retrieved using aggregation
public Flux<WordSet> getAll() {
AggregationOperation[] joinLanguages = new AggregationOperation[] {
lookup(mongoTemplate.getCollectionName(Language.class), "languageId", "_id", "language"),
unwind("language")
};
Aggregation agg = newAggregation(joinLanguages);
return mongoTemplate.aggregate(agg, WordSet.class, WordSet.class);
}
So, WordSet in mongodb contains only languageId and language field is filled using aggregation (join from Language collection).
But if I want to add more than one WordSet to collection I have an error
com.mongodb.MongoWriteException: E11000 duplicate key error collection: langdope.wordSet index: language.language_name_index dup key: { : null }
How can I handle it? I need languageName to be unique, but also I want to save objects which contains Languages without this problems. I can't just mark language as #Transient (aggregation will not work).

Resources