Hibernate saves child entity with null parent id - spring

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.

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.

Hibernate/Kotlin/Spring - ManyToMany cant delete items

I have two tables that are linked with #ManyToMany .I want that when deleting items that it deletes only own items and not the connected ones. I have 2 Classes. In Class "A" there is no issue , it deletes only its own items and not the connected one (the ones that represent class B). But when i delete items from class "B" i am getting error that is not possible because of foreign key. I have tried to use CascadeType.REMOVE , which will work but it will also delete the connected items from class "A" which i don't want to.
Class A:
#Entity
#TypeDef(name = "jsonb", typeClass = JsonBinaryType::class)
data class Student (
.....
#ManyToMany( fetch = FetchType.LAZY , cascade = [
CascadeType.PERSIST,
CascadeType.MERGE
])
#JoinTable(name = "student_teacher",
joinColumns = [JoinColumn(name = "student_id", referencedColumnName = "id")],
inverseJoinColumns = [JoinColumn(name = "teacher_id", referencedColumnName = "id")]
)
#JsonIgnore
val teachers: MutableSet<Teacher> ,
)
{
val teachersList: MutableSet<Teacher>
get() = teachers
}
Class B:
#Entity
#TypeDef(name = "list-array", typeClass = ListArrayType::class)
data class Teacher(
.....
){
#ManyToMany(mappedBy = "teachers",fetch = FetchType.LAZY)
#JsonIgnore
val students: MutableSet<Student> = MutableSet<Student>()
}
Got it work this way:
#Entity
#TypeDef(name = "list-array", typeClass = ListArrayType::class)
data class Teacher(
.....
){
#ManyToMany(mappedBy = "teachers",fetch = FetchType.LAZY)
#JsonIgnore
val students: MutableSet<Student> = MutableSet<Student>()
#PreRemove
fun removeTeacher() {
for (student in students) {
student.teachersList.remove(this)
}
}
}

Spring boot JPA - Insert or update a list of entities

I have a repo with a unique constraint on 2 fields, connection_id and token_type:
#Entity
#Table(
name = "business_api_token",
schema = "public",
uniqueConstraints = {
#UniqueConstraint(
name = "business_api_token_unique_connection_id_and_token_type",
columnNames = {"connection_id", "token_type"}
)
}
)
public class BusinessApiToken {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#OneToOne
#JoinColumn(
name = "connection_id",
nullable = false,
foreignKey = #ForeignKey(
name = "fk_business_api_token_connection_id"
)
)
private AccountingConnection connection;
#Column(name = "token_type")
#Enumerated(EnumType.STRING)
private ApiTokenType tokenType;
#Column(name = "token_value")
private String tokenValue;
...
}
I saw some posts saying add a custom query, something like this:
#Modifying
#Query("update User u set u.firstname = ?1, u.lastname = ?2 where u.id = ?3")
void setUserInfoById(String firstname, String lastname, Integer userId);
But how would I do this for a list? I was doing this:
businessApiTokenRepository.saveAll(tokens)
Which gives an error.
The tokens are created elsewhere without knowledge of existing ones, I can do another query to check first but that seems inefficient, and I have to do this all over.
Thanks

Manage the update order for queries in JPA

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.

Unable to save data to composite Table Via Spring Data rest json post

I have 3 Tables in db
training
- training_id (pk)
user_profile
- profile_id (pk)
-training_profile (composite table)
- training_id
- profile_id
I have already record in user_profile table having profile_id=44 and want to create new record for training table ,and also to associate this new training with already existing user_profile record which has id 44,but after post data is saved to training table but it is not inserted into lookup table user_training.
My Object Classes Are
- Training Class
#Entity
#Table(name = "training", schema = "public")
public class Training implements java.io.Serializable {
#Id #GeneratedValue
#Column(name = "training_id", unique = true, nullable = false)
private Long trainingId;
#ManyToMany(fetch = FetchType.LAZY, mappedBy = "trainings")
private Set<UserProfile> userProfiles = new HashSet<UserProfile>(0);
#Column(name = "training_subject", length = 200)
private String trainingSubject;
public Training() {
}
public Long getTrainingId() {
return this.trainingId;
}
public void setTrainingId(Long trainingId) {
this.trainingId = trainingId;
}
public String getTrainingSubject() {
return this.trainingSubject;
}
public void setTrainingSubject(String trainingSubject) {
this.trainingSubject = trainingSubject;
}
public Set<UserProfile> getUserProfiles() {
return this.userProfiles;
}
public void setUserProfiles(Set<UserProfile> userProfiles) {
this.userProfiles = userProfiles;
}
}
UserProfile
#Entity
#Table(name = "user_profile", schema = "public")
public class UserProfile implements java.io.Serializable {
#Id #GeneratedValue
#Column(name = "profile_id", unique = true, nullable = false)
private Long profileId;
#Column(name = "profile_description")
private String profileDescription;
#ManyToMany(fetch = FetchType.EAGER, cascade = { CascadeType.ALL })
#JoinTable(name = "user_training", schema = "public", joinColumns = {
#JoinColumn(name = "profile_id", nullable = false, updatable = false) }, inverseJoinColumns = {
#JoinColumn(name = "training_id", nullable = false, updatable = false) })
private Set<Training> trainings = new HashSet<Training>(0);
public UserProfile() {
}
public String getProfileDescription() {
return this.profileDescription;
}
public void setProfileDescription(String profileDescription) {
this.profileDescription = profileDescription;
}
public Set<Training> getTrainings() {
return this.trainings;
}
public void setTrainings(Set<Training> trainings) {
this.trainings = trainings;
}
}
My json post via postman
And Response I get
Response show that new training record inserted in table having training_id as 67
No association found for this new saved training
again it created new record for training and does not associate with existing user profile , I post curl -i -X POST -H "Content-Type:application/json" -d "{ \"trainingSubject\" : \"Oracle\", \"userProfiles\":[\"/userProfiles/44\"] }" http://localhost:8080/api/trainings
You could use the relative url assignment:
{
"trainingSubject": "oracle",
"userProfiles":["/userProfiles/44"]
}
Maybe also try with the full url: http://localhost:8080/api/userProfiles/44
EDITED
If you move the owning site of the ManyToMany relation to Training it will work with the above JSON. So currently the owner is allowed to set the realtions. If you do it like that:
#ManyToMany
#JoinTable(name = "user_training"
, joinColumns = {#JoinColumn(name = "profile_id") }
, inverseJoinColumns = {#JoinColumn(name = "training_id") })
private List<UserProfile> userProfiles = new ArrayList<>();
plus
#ManyToMany(mappedBy = "userProfiles")
private List<Training> trainings = new ArrayList<>();
Training owns the relation within userProfiles.
I think in your case it's the best option for now. Another option would be, when keeping the owner site at UserProfile on transactions, to update the relation there like:
PATCH http://localhost:8080/api/userProfiles/44
{
"trainings": ["trainings/66", "trainings/67"]
}
But with this you would need multible rest calls (1. POST new training and get the new Id 2. GET current training list 3. PATCH trainings list with newly added training)
Last option would be to add the REST-controller on your own.
Complete files for the first approach:
#Entity
#Table
public class Training implements Serializable {
#Id
#GeneratedValue
private Long trainingId;
#ManyToMany
#JoinTable(name = "user_training"
, joinColumns = {#JoinColumn(name = "profile_id") }
, inverseJoinColumns = {#JoinColumn(name = "training_id") })
private List<UserProfile> userProfiles = new ArrayList<>();
#Column(name = "training_subject", length = 200)
private String trainingSubject;
#Entity
#Table
public class UserProfile implements Serializable {
#Id
#GeneratedValue
private Long profileId;
#Column(name = "profile_description")
private String profileDescription;
#ManyToMany(mappedBy = "userProfiles")
private List<Training> trainings = new ArrayList<>();
public interface TrainingRepository extends JpaRepository<Training, Long> {
}
public interface UserProfileRepository extends JpaRepository<UserProfile, Long> {
}
With the upper JSON this will work, I tested it. You will not see the correct result directly in the response of curl-POST. To see the added relation you must follow the userProfiles-link like GET http://localhost:8080/transactions/<newId>/userProfiles

Resources