Spring JPA cannot map a field with custom setter in a Kotlin data class - spring

I've got a Kotlin data class with a custom setter. The Spring JPA framework cannot seem to map the property with the custom setter. If I remove the custom getter/setter and rename the property to login instead of _login, everything seems to work fine. How can I create the property in the Kotlin data class with a custom setter, so that it is recognised in the JPA framework?
User.kt
#Entity
#Table(name = "jhi_user")
#Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE)
data class User (
#Id
#GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "sequenceGenerator")
#SequenceGenerator(name = "sequenceGenerator")
var id: Long? = null,
#NotNull
#Pattern(regexp = Constants.LOGIN_REGEX)
#Size(min = 1, max = 50)
#Column(name = "login", length = 50, unique = true, nullable = false)
var _login: String? = null,
#JsonIgnore
#NotNull
#Size(min = 60, max = 60)
#Column(name = "password_hash",length = 60)
var password: String? = null,
...
#JsonIgnore
#ManyToMany
#JoinTable(
name = "jhi_user_authority",
joinColumns = arrayOf(JoinColumn(name = "user_id", referencedColumnName = "id")),
inverseJoinColumns = arrayOf(JoinColumn(name = "authority_name", referencedColumnName = "name")))
#Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE)
#BatchSize(size = 20)
var authorities: MutableSet<Authority>? = null): AbstractAuditingEntity(), Serializable {
//Lowercase the login before saving it in database
var login: String?
get() = _login
set(value) {
_login = StringUtils.lowerCase(value, Locale.ENGLISH)
}
}
The error I'm getting:
...
Caused by: java.lang.IllegalArgumentException: Unable to locate Attribute with the the given name [login] on this ManagedType [com.sample.domain.AbstractAuditingEntity]
at org.hibernate.metamodel.internal.AbstractManagedType.checkNotNull(AbstractManagedType.java:128)
at org.hibernate.metamodel.internal.AbstractManagedType.getAttribute(AbstractManagedType.java:113)
at org.hibernate.metamodel.internal.AbstractManagedType.getAttribute(AbstractManagedType.java:111)
at org.springframework.data.jpa.repository.query.QueryUtils.toExpressionRecursively(QueryUtils.java:569)
at org.springframework.data.jpa.repository.query.JpaQueryCreator$PredicateBuilder.getTypedPath(JpaQueryCreator.java:377)
at org.springframework.data.jpa.repository.query.JpaQueryCreator$PredicateBuilder.build(JpaQueryCreator.java:300)
at org.springframework.data.jpa.repository.query.JpaQueryCreator.toPredicate(JpaQueryCreator.java:205)
at org.springframework.data.jpa.repository.query.JpaQueryCreator.create(JpaQueryCreator.java:117)
at org.springframework.data.jpa.repository.query.JpaQueryCreator.create(JpaQueryCreator.java:54)
at org.springframework.data.repository.query.parser.AbstractQueryCreator.createCriteria(AbstractQueryCreator.java:111)
at org.springframework.data.repository.query.parser.AbstractQueryCreator.createQuery(AbstractQueryCreator.java:90)
at org.springframework.data.repository.query.parser.AbstractQueryCreator.createQuery(AbstractQueryCreator.java:78)
at org.springframework.data.jpa.repository.query.PartTreeJpaQuery$QueryPreparer.<init>(PartTreeJpaQuery.java:135)
at org.springframework.data.jpa.repository.query.PartTreeJpaQuery$CountQueryPreparer.<init>(PartTreeJpaQuery.java:256)
at org.springframework.data.jpa.repository.query.PartTreeJpaQuery.<init>(PartTreeJpaQuery.java:72)
at org.springframework.data.jpa.repository.query.JpaQueryLookupStrategy$CreateQueryLookupStrategy.resolveQuery(JpaQueryLookupStrategy.java:103)

Using custom setters for constructor parameters like this is a bit ugly (but unfortunately the only way I am aware of doing it).
For starters JPA is going to want to register both _login and login as separate columns in your database since neither of them are #Transient. I believe your issue arises here since you have marked the _login property to map to the column "login" whereas the login property has no #Column annotation so it is trying to map to it's default value of "login" which already has the _login property mapped to it.
Therefore I think you probably want to make _login transient and only persist login (I've missed out irrelevant code for brevity and clarity):
...
#Transient
var _login: String? = null,
...
#NotNull
#Pattern(regexp = Constants.LOGIN_REGEX)
#Size(min = 1, max = 50)
#Column(name = "login", length = 50, unique = true, nullable = false)
var login: String?
get() = _login
set(value) {
_login = StringUtils.lowerCase(value, Locale.ENGLISH)
}
If this still doesn't work then I really think it's more hassle than it's worth trying to get this already slightly hacky workaround for using custom setters on constructor properties working with JPA. I would suggest instead to use a #PrePersist/#PreUpdate method to do the lowercasing for you prior to saving it to the database.

Related

Spring Data JPA Repository function does not work in Test

I have a question regarding Spring Data JPA.
To make it as simple as possible I made up a very simple example.
We have the TestUser, that can have a FavouriteColor, but his favouriteColor can also be null.
TestUser.kt
#Entity
class TestUser(
#Id
#Column(name = "TestUserId")
var userId: Long,
#Column(name = "Name")
var name: String,
#Column(name = "FavouriteColorId")
var favouriteColorId: Long? = null,
#OneToOne
#JoinColumn(
name = "FavouriteColorId",
referencedColumnName = "FavouriteColorId",
insertable = false,
updatable = false,
nullable = true
)
var favouriteColor: FavouriteColor? = null
)
FavouriteColor.kt
#Entity
class FavouriteColor(
#Id
#Column(name = "FavouriteColorId")
var favouriteColorId: Long,
#Column(name = "ColorCode")
var colorCode: String
)
When I search for the users that have a favourite Color by findTestUsersByFavouriteColorNotNull(), the size of the result is 0. Even if there is an User that has a favourite color. And when I use findAll() and then apply the filter, the result is correct.
StackOverflowTest.kt
#SpringBootTest
#Transactional
class StackOverflowTest {
#Autowired
lateinit var testUserRepository: TestUserRepository
#Autowired
lateinit var favouriteColorRepository: FavouriteColorRepository
#Test
fun testFilter() {
val favouriteColor = FavouriteColor(favouriteColorId = 0L, colorCode = "#000000")
favouriteColorRepository.save(favouriteColor)
val user = testUserRepository.save(TestUser(userId = 0L, name = "Testuser"))
user.favouriteColor = favouriteColor
testUserRepository.save(user)
val usersWithColor1 = testUserRepository.findAll().filter { it.favouriteColor != null }
assert(usersWithColor1.size == 1) // This assertion is correct
val usersWithColor2 = testUserRepository.findTestUsersByFavouriteColorIdIsNotNull()
assert(usersWithColor2.size == 1) // This assertion fails
val usersWithColor3 = testUserRepository.findTestUsersByFavouriteColorIsNotNull()
assert(usersWithColor3.size == 1) // This assertion fails
}
}
Update:
I added the Repository function findTestUsersByFavouriteColorIdNotNull() but it also does not work
Update2:
I updated the functions to findTestUsersByFavouriteColorIdIsNotNull and findTestUsersByFavouriteColorIsNotNull, but the assertions are still failing
Can somebody explain me, why the findTestUsersByFavouriteColorNotNull() does not work ? And is there some way to get this function working in the tests?
Thanks :)
I'm suspecting that happen because you have 2 variables of the same column name
#Column(name = "FavouriteColorId")
var favouriteColorId: Long? = null,
#OneToOne
#JoinColumn(
name = "FavouriteColorId",
referencedColumnName = "FavouriteColorId",
insertable = false,
updatable = false,
nullable = true
)
var favouriteColor: FavouriteColor? = null
Try removing one of the variable, and try again.

Spring data rest ManyToMany mapping PUT/update operation is not replacing the nested object

I started to learn spring data rest. I'm doing PUT operation and it's not working for the nested objects for ManyToMany relationship, whereas it works fine for OneToMany relation.
Entities structures:
#Table(name="CONFIG_DTLS",schema = "app_txn")
#Entity
public class Config {
#Id
#GenericGenerator(name = "UUIDGenerator", strategy = "uuid2")
#GeneratedValue(generator = "UUIDGenerator")
#Column(name = "ID", updatable = false, nullable = false)
private UUID id;
#Column(name = "NAME", nullable = false, length = 75)
private String name;
/*Unable to replace the data in the MBR_CONFIG_MAPPING table in the put operation.
When the control comes to #HandleBeforeSave annotated method in PUT operation,
the request data contains the existing Member info instead of the one which i'm passing in the PUT request body */
#ManyToMany(cascade = {CascadeType.PERSIST, CascadeType.MERGE},fetch = FetchType.EAGER)
#JoinTable(schema = "app_txn", name = "MBR_CONFIG_MAPPING",
joinColumns ={#JoinColumn(name="CONFIG_ID",referencedColumnName = "ID")},
inverseJoinColumns = {#JoinColumn(name="MBR_ID",referencedColumnName = "ID")}
)
private Set<Member> members;
//able to replace the notifications completely in PUT operation
#OneToMany(cascade = CascadeType.ALL,fetch = FetchType.EAGER)
#JoinColumn(name = "CONFIG_ID",referencedColumnName = "ID")
private Set<Notification> notifications;
}
Member.java
#Table(name="MBR_DTLS",schema = "app_txn")
#Entity
public class Member {
#Id
#GenericGenerator(name = "UUIDGenerator", strategy = "uuid2")
#GeneratedValue(generator = "UUIDGenerator")
#Column(name = "ID", updatable = false, nullable = false)
private UUID id;
#Column(name = "OTHER_MBR_DATA", updatable = false)
private String otherMbrData;
}
Notification.java
#Table(name="NOTIFICATIONS",schema = "app_txn")
#Entity
public class Notification {
#Id
#GenericGenerator(name = "UUIDGenerator", strategy = "uuid2")
#GeneratedValue(generator = "UUIDGenerator")
#Column(name = "ID", updatable = false, nullable = false)
private UUID id;
#Column(name="LEVEL")
private String level;
#Column(name="EMAIL")
private String email;
}
Interfaces:
#RepositoryRestResource(collectionResourceRel = "configs", path="configs")
public interface ConfigRepo extends PagingAndSortingRepository<Config,UUID> {
}
#RepositoryRestResource(exported=false) // don't want to users to manipulate it directly.
public interface MemberRepo extends PagingAndSortingRepository<Member,Object> {
}
Here I don't want to add or modify anything in the MBR_DTLS table as it is loaded by another backend process. I want to update only the mapping details MBR_CONFIG_MAPPING table whenever user does the PUT/update operation. POST/create operation is working fine. Please share your thoughts on how to fix this and if you have any questions add it in the comment section.
PS: I referred some links online but that does not help much - Spring Data REST - PUT request does not work properly since v.2.5.7

Creating subcategories in kotlin spring boot

I need to implement categories and subcategories within my entities. Here's what I have so far and think it should be:
StockCategory.kt
#Entity
#Table(name = "table_categories")
data class StockCategory(
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "category_id")
val id: Long? = null,
#ManyToOne
#JoinColumn(name = "parentid")
val parent: StockCategory? = null,
#ManyToMany(mappedBy = "categories")
var stockItems: MutableList<StockItem> = mutableListOf(),
#OneToMany(mappedBy = "parent")
var childCategories: MutableList<StockCategory> = mutableListOf(),
)
StockItem.kt
#Entity
#Table(name = "table_stock")
data class StockItem(
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "stock_item_id")
val id: Long? = null,
#Column(name = "stock_item_name")
var name: String = "New Item",
...
#ManyToMany
#JoinColumn(name = "item_category", referencedColumnName = "category_id")
var categories: MutableList<StockCategory> = mutableListOf(),
...
)
Now at the moment, this looks to be correct... At the very least Spring Boot is not complaining.
However, in terms of what to do next, I'm not sure. I know I need to implement a JpaRepository, of which I have the current:
StockCategoryRepository.kt
interface StockCategoryRepository: JpaRepository<StockCategory, Long> {
}
I also need to implement the relevant methods in my service class.
What exactly do I need to do next in order to get this to work and be able to use the information later on? Please also ELI5 too as although I have a decent amount of knowledge on this, I'm still not where I would like to be when it comes to this.
A few background bits if it makes it easier for you.
I'm using H2 as my database, Spring Boot and Kotlin as my language.

java-graphql - Unable to match type definition with java type

I am using graphql-spring-boot to serve graphql queries from my spring-boot project. Right now I am working on matching the graphql scheme type definitions with my spring entites. For whatever reason, I am getting the following error:
Caused by: com.coxautodev.graphql.tools.SchemaClassScannerError: Unable to match type definition (ListType{type=TypeName{name='HomestayInfo'}}) with java type (class ninja.familyhomestay.domain.HomestayInfo): Java class is not a List or generic type information was lost: class ninja.familyhomestay.domain.HomestayInfo
at com.coxautodev.graphql.tools.TypeClassMatcher.error(TypeClassMatcher.kt:19)
at com.coxautodev.graphql.tools.TypeClassMatcher.match(TypeClassMatcher.kt:79)
at com.coxautodev.graphql.tools.TypeClassMatcher.match(TypeClassMatcher.kt:25)
Here's my graphql schema defintion for HomestayInfo:
type HomestayInfo{
homestayName: String
homestayShortDescription: String
homestayDescription: String
address: Address
rooms: [Room]
houseImages: [HouseImage]
pets: [Pet]
}
and the corresponding kotlin entity:
#Entity
#Table(name = "homestay_info")
#Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE)
#Document(indexName = "homestay_info")
data class HomestayInfo(
#Id
#GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "sequenceGenerator")
#SequenceGenerator(name = "sequenceGenerator")
var id: Long? = null,
#Column(name = "homestay_name")
var homestayName: String? = null,
#Column(name = "homestay_short_description")
var homestayShortDescription: String? = null,
#Column(name = "homestay_description")
var homestayDescription: String? = null,
#OneToOne
#JoinColumn(name = "address_id")
var address:Address?=null,
#OneToMany(mappedBy = "homestayInfo", cascade = [CascadeType.ALL], fetch = FetchType.LAZY)
var rooms: MutableSet<Room> = HashSet(),
#OneToMany(mappedBy = "homestayInfo", cascade = [CascadeType.ALL], fetch = FetchType.LAZY)
var houseImages: MutableSet<HouseImage> = HashSet(),
#OneToMany(mappedBy = "homestayInfo", cascade = [CascadeType.ALL], fetch = FetchType.LAZY)
var pets: MutableSet<Pet> = HashSet()
) : Serializable
I don't see anything wrong with the mapping. Any ideas?
Adding scalar Date to the top of your schema.graphqls file should do the trick! So your file would look like such:
scalar Date
schema { // not sure if your file has this, but mine does
query: Query
}
type HomestayInfo{
...

null id generated for composite PK

I have the following tables and the following relationship table too: , which has a composite PK as follow:
UserRole.java
#RooJavaBean
#RooJpaEntity(identifierType = UserRolePK.class, versionField = "", table = "UserRole", schema = "dbo")
#RooDbManaged(automaticallyDelete = true)
#RooToString(excludeFields = { "idApplication", "idRole", "idUserName" })
public class UserRole {
}
UserRole_Roo_DbManaged.aj
#ManyToOne
#JoinColumn(name = "IdApplication", referencedColumnName = "IdApplication", nullable = false, insertable = false, updatable = false)
private Application UserRole.idApplication;
#ManyToOne
#JoinColumn(name = "IdRole", referencedColumnName = "IdRole", nullable = false, insertable = false, updatable = false)
private Role UserRole.idRole;
#ManyToOne
#JoinColumn(name = "IdUserName", referencedColumnName = "IdUserName", nullable = false, insertable = false, updatable = false)
private Users UserRole.idUserName;
But also exist a PK table:
#RooIdentifier(dbManaged = true)
public final class UserRolePK {}
And its identifier class (UserRolePK_Roo_Identifier.aj)
privileged aspect UserRolePK_Roo_Identifier {
declare #type: UserRolePK: #Embeddable;
#Column(name = "IdRole", nullable = false)
private Long UserRolePK.idRole;
#Column(name = "IdUserName", nullable = false, length = 16)
private String UserRolePK.idUserName;
#Column(name = "IdApplication", nullable = false)
private Long UserRolePK.idApplication;
The way how I'm setting the service objec to save is:
UserRole userRole= new UserRole();
userRole.setIdApplication(app);
userRole.setIdRole(invited);
userRole.setIdUserName(user);
appService.saveURole(userRole);
app has been set and saved before (same transaction), as well as invited and user objects.
Since user (from Users table with composite PK: IdUserName which is a String ), is defined as follow, otherwise doesnt work.
#RooJavaBean
#RooJpaEntity(versionField = "", table = "Users", schema = "dbo")
#RooDbManaged(automaticallyDelete = true)
#RooToString(excludeFields = { "quotations", "taxes", "userRoles", "idCompany", "idPreferredLanguage" })
public class Users {
#Id
//#GeneratedValue(strategy = GenerationType.AUTO)
#Column(name = "IdUserName", length = 16, insertable = true, updatable = true)
private String idUserName;
}
So, the error that I'm getting is:
org.springframework.orm.jpa.JpaSystemException: org.hibernate.id.IdentifierGenerationException: null id generated for:class com.domain.UserRole; nested exception is javax.persistence.PersistenceException: org.hibernate.id.IdentifierGenerationException: null id generated for:class com.domain.UserRole
Try this:
public class UserRole {
#PrePersist
private void prePersiste() {
if (getId() == null) {
UserRolePK pk = new UserRolePK();
pk.setIdApplication(getIdApplication());
pk.setIdRole(getIdRole);
pk.setIdUserName(getIdUserName());
setId(pk);
}
}
}
Roo is generating the fields on UserRole entity and its id embedded class, but is not the same thing (UserRole.idRole is not the same than UserRole.id.idRole). In your example, you fill the UserRole fields, but not the id fields. This code makes it for you before entity is persisted.
Good luck!
In my case if the follow example tries to be persisted in DB, then similar Exception mentioned above is thrown:
EntityExample e = new EntityExample();
repositoryExample.save(e);
//throw ex
This is caused due to missing id field values which needs to be set something like that:
EntityExample e = new EntityExample();
e.setId(new EmbeddedIdExample(1, 2, 3));
repositoryExample.save(e);

Resources