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.
Related
I have these entities
#Entity(name = "inspiration")
class InspirationEntity(
#Id
var uuid: UUID? = null,
#Column(name = "display_name")
var displayName: String,
#CreationTimestamp
#Column(name = "created_at")
#Temporal(TemporalType.TIMESTAMP)
var createdAt: Date?,
#UpdateTimestamp
#Temporal(TemporalType.TIMESTAMP)
#Column(name = "last_modified_date")
var lastModifiedDate: Date?,
#OneToMany(targetEntity = BaseSliderEntity::class)
#Cascade(CascadeType.ALL)
var sliderList: List<BaseSliderEntity>,
)
#Entity(name = "base_slider")
#Inheritance(strategy = InheritanceType.JOINED)
//TODO investigate DiscriminatorColumn anotation and best practices, because it shows some warning in logs
#DiscriminatorColumn(name = "slider_type", discriminatorType = DiscriminatorType.STRING)
abstract class BaseSliderEntity(
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
open var id: Long? = null,
)
#Entity(name = "dish_of_the_day")
#DiscriminatorValue("DISH_OF_THE_DAY")
class DishOfTheDayEntity(
#Column(name = "title_en")
var titleEN: String,
#Column(name = "title_de")
var titleDE: String,
) : BaseSliderEntity()
#Entity(name = "inspiration_screen_link")
#DiscriminatorValue("LINK")
class InspirationScreenLinkEntity(
#Enumerated(EnumType.STRING)
var destination: Destination,
) : BaseSliderEntity()
this is my dto
data class InspirationDTO(
#JsonProperty(access = JsonProperty.Access.READ_ONLY)
var uuid: UUID?,
#JsonProperty(access = JsonProperty.Access.READ_ONLY)
var createdAt: Date?,
#JsonProperty(access = JsonProperty.Access.READ_ONLY)
var lastModifiedDate: Date?,
val displayName: String,
val inspirationScreenItemList: List<InspirationScreenItemDTO>,
)
and this is InspirationScreenItemDTO
#JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type")
#JsonSubTypes(
JsonSubTypes.Type(value = DishOfTheDayDTO::class, name = "DishOfTheDay"),
JsonSubTypes.Type(value = InspirationScreenContinuousSliderDTO::class, name = "InspirationScreenContinuousSlider"),
JsonSubTypes.Type(value = InspirationScreenLinkDTO::class, name = "InspirationScreenLink"),
JsonSubTypes.Type(value = InspirationScreenPagingSliderDTO::class, name = "InspirationScreenPagingSlider"),
JsonSubTypes.Type(
value = InspirationScreenRecentlyViewedSliderDTO::class,
name = "InspirationScreenRecentlyViewedSlider"
),
JsonSubTypes.Type(value = InspirationScreenTagsSliderDTO::class, name = "InspirationScreenTagsSlider"),
)
open class InspirationScreenItemDTO
when I try to update
like this
#Transactional
fun update(uuid: UUID, inspirationDTO: InspirationDTO): InspirationDTO {
var entity = inspirationRepository.findById(id).get()
val updatedEntity: InspirationEntity = inspirationMapper.convertToEntity(inspirationDTO)
entity.sliderList = updatedEntity.sliderList
val result: InspirationEntity = inspirationRepository.save(entity)
return inspirationMapper.convertToDto(result)
}
this is my swagger post
{
"displayName": "ED",
"inspirationScreenItemList": [
{
"type": "DishOfTheDay",
"titleEN": "GGGGGGG",
"titleDE": "GGGGGGG"
}
]
}
in DishofDto table it creates two rows first and updated, and inspiration remains one row which is okay but dish of the day shouldn't contain two rows, it should be just updated one.
My solution was to add uuid id as primary key in inspiration which works with deleting by id and then put toUpdateDto under the same id and save , but I'm not sure it's good solution.
I am creating a simple kanban application as following, each kanban is made out of a sequence of stages and each stage have a level field to define its position. I want to be able to add, move and remove stages at will so I have to keep the level of each stage consistent, simple enough.
#Entity
#Table(name = "kanbans")
data class Kanban (
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "id", nullable = false)
var id: Int? = null,
#get:NotNull
#get:NotBlank
#Column(name = "name", nullable = false)
var name: String? = null,
#get:NotNull
#get:NotBlank
#Column(name = "description", nullable = false)
var description: String? = null,
#get:NotNull
#Column(name = "closed", nullable = false)
var closed: Boolean? = null,
#get:NotNull
#Column(name = "created_at", nullable = false)
var createdAt: LocalDateTime? = null,
#get:NotNull
#Column(name = "updated_at", nullable = false)
var updatedAt: LocalDateTime? = null,
)
#Entity
#Table(name = "stages")
data class Stage (
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "id", nullable = false)
var id: Int? = null,
#get:NotNull
#get:NotBlank
#Column(name = "name", nullable = false)
var name: String? = null,
#get:NotNull
#get:NotBlank
#Column(name = "description", nullable = false)
var description: String? = null,
#get:NotNull
#Column(name = "closed", nullable = false)
var closed: Boolean? = null,
#get:NotNull
#Column(name = "level", nullable = false)
var level: Int? = null,
#OneToMany(fetch = FetchType.LAZY, mappedBy = "stage")
var tasks: List<Task> = ArrayList(),
#get:NotNull
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "kanban_id", nullable = false)
var kanban: Kanban? = null,
#get:NotNull
#Column(name = "created_at", nullable = false)
var createdAt: LocalDateTime? = null,
#get:NotNull
#Column(name = "updated_at", nullable = false)
var updatedAt: LocalDateTime? = null,
)
When creating the first stage its always assigning its level at 0 and then when adding new ones the level will define the stage position at the list of stages. The problem is that when I try to update the previous existing stages to give place to the new one, the only way I found to make this work is to place a saveAndFlush call in a loop but I find it to be not a good ideia.
#Repository
interface StageRepository : JpaRepository<Stage, Int> {
fun findAllByKanbanAndLevelGreaterThanEqualOrderByLevelDesc(kanban: Kanban, level: Int): List<Stage>
#Modifying
#Transactional
#Query("UPDATE Stage s SET s.level = s.level + 1 WHERE s.kanban = :kanban AND s.level >= :level")
fun incrementLevelForKanbanStagesWhereLevelIsGreaterThan(kanban: Kanban, level: Int)
}
the incrementLevelForKanbanStagesWhereLevelIsGreaterThan method fails as the database have a unique constraint to level and kanban_id with the following error:
Caused by: org.postgresql.util.PSQLException: ERROR: duplicate key value violates unique constraint "stages_kanban_id_level_key"
Detalhe: Key (kanban_id, level)=(337, 1) already exists.
this is obviously happening because it is trying to update level 0 to level 1 before updating level 1 to level 2 and so I have tried:
#Transactional
#Query("UPDATE Stage s SET s.level = s.level + 1 WHERE s.kanban = :kanban AND s.level >= :level ORDER BY s.level DESC")
fun incrementLevelForKanbanStagesWhereLevelIsGreaterThan(kanban: Kanban, level: Int)
which does not compile,
#Service
#Transactional
class StageCrudService: CrudService<Stage, Int, StageRepository, StageValidationService>() {
#Throws(ValidationException::class)
override fun create(model: Stage): Stage {
prepareToCreate(model)
validationService.canSave(model)
incrementKanbanStageLevels(model)
return repository.save(model)
}
private fun prepareToCreate(model: Stage) {
val now = LocalDateTime.now()
val closed = model.closed ?: false
model.closed = closed
model.createdAt = now
model.updatedAt = now
model.level = model.level ?: 0
}
private fun incrementKanbanStageLevels(model: Stage) {
val level = model.level ?: 0
val stages = repository.findAllByKanbanAndLevelGreaterThanEqualOrderByLevelDesc(model.kanban!!, level)
stages.forEach { stage ->
stage.level = stage.level?.plus(1)
}
repository.saveAll(stages)
repository.flush()
}
}
and
private fun incrementKanbanStageLevels(model: Stage) {
val level = model.level ?: 0
val stages = repository.findAllByKanbanAndLevelGreaterThanEqualOrderByLevelDesc(model.kanban!!, level)
stages.forEach { stage ->
stage.level = stage.level?.plus(1)
repository.save(stage)
}
repository.flush()
}
but both fails the same way as the query. Now the question is:
Is there a better way to manage the update order for this kind of situation instead of doing:
private fun incrementKanbanStageLevels(model: Stage) {
val level = model.level ?: 0
val stages = repository.findAllByKanbanAndLevelGreaterThanEqualOrderByLevelDesc(model.kanban!!, level)
stages.forEach { stage ->
stage.level = stage.level?.plus(1)
repository.saveAndFlush(stage)
}
}
It seems to me that you are possibly trying to implement something that can be managed for you via the JPA #OrderColumn annotation:
https://docs.oracle.com/javaee/7/api/javax/persistence/OrderColumn.html
Specifies a column that is used to maintain the persistent order of a
list. The persistence provider is responsible for maintaining the
order upon retrieval and in the database. The persistence provider is
responsible for updating the ordering upon flushing to the database to
reflect any insertion, deletion, or reordering affecting the list.
To use this you would need to make the relationship bi-directional and the level should be maintained by your JPA provider as items are added to and removed from the list
#Entity
#Table(name = "kanbans")
data class Kanban (
.....
#get:NotNull
#get:NotBlank
#OrderColumn(name = "level")
#OneToMany(fetch = FetchType.LAZY, mappedBy = "kanban")
var stage: List<Stage> = ArrayList()
.....
}
So you can then remove and add items (at any position) and the sequence will be maintained for you.
I have two classes which have bidirectional ManyToMany relations in a Spring Boot application. When I would like to fetch my entities, they start recursively looping, and I get a stackoverflow exception. These is my implementation.
#Entity
#Table(name = "route")
data class Route(
#Column(name = "uid")
#Type(type = "pg-uuid")
#Id
var uid: UUID,
var image: String,
#Column(name = "rate_id")
var rate_id: UUID,
#ManyToMany(cascade = [CascadeType.ALL], fetch = FetchType.LAZY)
#JoinTable(name = "ach",
joinColumns = [JoinColumn(name = "route_id", referencedColumnName = "uid")],
inverseJoinColumns = [JoinColumn(name = "athlete_id", referencedColumnName = "uid")])
var athletes: List<Athlete> = mutableListOf())
#Entity
#Table(name = "athlete")
data class Athlete(
#Column(name = "uid")
#Type(type = "pg-uuid")
#Id
var uid: UUID,
var email: String,
var image: String,
#ManyToMany(mappedBy = "athletes")
var routes: List<Route> = mutableListOf())
I understand that the problem is that both of my list attribute is in the constructor. However I would like to have the list attributes in the response. I have seen solutions where the toString method was overwritten to create a json string. I would prefer to return an object instead of a jsonString. Is there a way to implement the above problem with or without dataclasses? Please give some example if there is a way.
Please notice that this answer is solution for Kotlin data classes with ManyToMany bidirectional relation.
#ManyToMany(cascade = [CascadeType.ALL], fetch = FetchType.LAZY)
#JoinTable(name = "ach",
joinColumns = [JoinColumn(name = "route_id", referencedColumnName = "uid")],
inverseJoinColumns = [JoinColumn(name = "athlete_id", referencedColumnName = "uid")])
#JsonIgnoreProperties("routes")
var athletes: List<Athlete> = mutableListOf())
#ManyToMany(mappedBy = "athletes")
#JsonIgnoreProperties("athletes")
var routes: List<Route> = mutableListOf())
With adding the #JsonIgnoreProperties, you can avoid the recursive loop.
For me the above solution didn't work. Instead I had to override the equals and hashCode methods to avoid the recursion like so:
#Entity
#Table(name = "route")
data class Route(
#Column(name = "uid")
#Type(type = "pg-uuid")
#Id
var uid: UUID,
var image: String,
#Column(name = "rate_id")
var rate_id: UUID,
#ManyToMany(cascade = [CascadeType.ALL], fetch = FetchType.LAZY)
#JoinTable(name = "ach",
joinColumns = [JoinColumn(name = "route_id", referencedColumnName = "uid")],
inverseJoinColumns = [JoinColumn(name = "athlete_id", referencedColumnName = "uid")])
var athletes: List<Athlete> = mutableListOf()) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as Route
if (uid != other.uid) return false
if (image != other.image) return false
if (rate_id != other.rate_id) return false
return true
}
override fun hashCode(): Int {
var result = uid
result = 31 * result + image.hashCode()
result = 31 * result + rate_id.hashCode()
return result
}
}
By default when you generate the equals and hashCode (by right-clicking > "Generate..." > "equals() and hashCode()" it will look like this:
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as Route
if (uid != other.uid) return false
if (image != other.image) return false
if (rate_id != other.rate_id) return false
if (route != other.route) return false
return true
}
override fun hashCode(): Int {
var result = uid
result = 31 * result + image.hashCode()
result = 31 * result + rate_id.hashCode()
result = 31 * result + route.hashCode()
return result
}
You have to remove the route object from both methods to stop the recursion.
IMPORTANT: You can do this on either side (Athlete or Route) to get the same result.
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.
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);