Manage the update order for queries in JPA - spring

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.

Related

How to do update?

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.

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 Boot JPA - Projection selecting all fields from table when has a collection

I trying to get two fields and a #ElementCollection from entity using projection with interface, but the JPA are selecting all fields from my entity and when i remove the method that get the list of my #ElementCollection the JPA select the only two fields.
My entity class:
#Entity(name = "users")
data class User(
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
var id: Long? = null,
#Column(nullable = false)
var name: String? = null,
#Column(unique = true, nullable = false)
var cpf: String? = null,
#Column(name = "phone_number", nullable = false)
var phoneNumber: String? = null,
#Column(unique = true, nullable = false)
var email: String? = null,
#Column(name = "password_hash")
var passwordHash: String? = null,
#Column(name = "password_hash_recovery")
var passwordHashRecovery: String? = null,
#Column(name = "password_hash_recovery_date")
var passwordHashRecoveryDate: String? = null,
#Column(name = "self_employed", nullable = false)
var selfEmployed: Boolean? = null,
#JoinColumn(name = "user_photo", referencedColumnName = "id")
#OneToOne(fetch = FetchType.LAZY)
var userPhoto: File? = null,
#JoinColumn(name = "id_location", referencedColumnName = "id")
#OneToOne(fetch = FetchType.LAZY)
var location: Location? = null,
#Column(name = "rating_star", nullable = false)
#Enumerated(EnumType.STRING)
var ratingStar: RatingStar = RatingStar.ONE,
#JoinColumn(name = "id_area", referencedColumnName = "id")
#OneToOne(fetch = FetchType.LAZY)
var area: Area? = null,
#OneToMany(mappedBy = "user", cascade = [CascadeType.ALL], fetch = FetchType.LAZY)
var workTimes: List<WorkTime> = arrayListOf(),
#OneToMany(mappedBy = "contractor", cascade = [CascadeType.ALL], fetch = FetchType.LAZY)
var contractorOrders: List<Order>? = arrayListOf(),
#OneToMany(mappedBy = "selfEmployed", cascade = [CascadeType.ALL], fetch = FetchType.LAZY)
var selfEmployedOrders: List<Order>? = arrayListOf(),
) {
#ElementCollection(fetch = FetchType.EAGER)
#Enumerated(EnumType.STRING)
#CollectionTable(
name = "profiles_authorties",
joinColumns = [JoinColumn(name = "user_id", referencedColumnName = "id")],
)
#Column(name = "authority")
private val _authorities: MutableSet<ProfileAuthorities> = HashSet()
init {
_authorities.add(ProfileAuthorities.CLIENT)
}
fun setAsAdmin() =
_authorities.add(ProfileAuthorities.ADMIN)
fun getAuthorities(): Set<ProfileAuthorities> = _authorities
}
My interface for projection:
interface LoginUserProjection {
fun getId(): Long
fun getPasswordHash(): String
fun getAuthorities(): Set<ProfileAuthorities>
}
The result query is:
Hibernate: select user0_.id as id1_12_, user0_.id_area as id_area11_12_, user0_.cpf as cpf2_12_, user0_.email as email3_12_, user0_.id_location as id_loca12_12_, user0_.name as name4_12_, user0_.password_hash as password5_12_, user0_.password_hash_recovery as password6_12_, user0_.password_hash_recovery_date as password7_12_, user0_.phone_number as phone_nu8_12_, user0_.rating_star as rating_s9_12_, user0_.self_employed as self_em10_12_, user0_.user_photo as user_ph13_12_ from users user0_ where user0_.id=?
Hibernate: select authoriti0_.user_id as user_id1_8_0_, authoriti0_.authority as authorit2_8_0_ from profiles_authorties authoriti0_ where authoriti0_.user_id=?
when i remove fun getAuthorities(): Set<ProfileAuthorities> from LoginUserProjection the result is:
Hibernate: select user0_.id as col_0_0_, user0_.password_hash as col_1_0_ from users user0_ where user0_.id=?
My repository method:
#Repository
interface UserRepository : JpaRepository<User, Long> {
fun <T> getUserProjectionById(id: Long, projection: Class<T>): T?
}

Hibernate saves child entity with null parent id

Hibernate doesn't want to save IDs for child entities. I have the following tables:
#Entity
#Table(name = "ct_orders")
data class Order(
#Id
#Column(name = "id")
#GeneratedValue(strategy = javax.persistence.GenerationType.IDENTITY)
val id: Int = 0,
#OneToMany(fetch = FetchType.LAZY, cascade = arrayOf(CascadeType.ALL), mappedBy = "order")
val route: List<Route>? = null,
...
)
#Entity
#Table(name = "ct_routes")
#JsonIgnoreProperties("id", "order")
data class Route(
#Id
#Column(name = "id")
#GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Int = 0,
#Column
val location: Point = GeoHelpers.point(),
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "order_id")
val order: Order? = null,
#Column
val title: String = ""
)
ct_routes saving with null in order_id. Is there some problem with relationships? Or, may be there is something wrong in my code?
Here is the part of code, which saves an Order entity:
val order = orderRepository.save(Order(
...
route = GeoHelpers.placesListToEntities(data.places),
...
))
fun placesListToEntities(points: List<PlaceDto>) = points.map {
Route(
location = Helpers.geometry(it.location.latitude, it.location.longitude),
title = it.title
)
}
You're modeling bidirectional #OneToMany and as shown in the example in the documentation you're responsible for setting the parent value on the child entity:
val order = orderRepository.save(Order(...).apply{
...
route = GeoHelpers.placesListToEntities(this, data.places),
...
})
fun placesListToEntities(order:Order, points: List<PlaceDto>) = points.map {
Route(
order = order,
location = Helpers.geometry(it.location.latitude, it.location.longitude),
title = it.title
)
}
PS. Since Route is an entity you could change your model a bit to enforce the constraints on the langauge level i.e:
class Route internal constructor() {
lateinit var order: Order
constructor(order: Order) : this() {
this.order = order
}
}
See this question for more details.

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