Edit: This code actually works correctly. The problem was un-related and was due to a conflicting Entity which was creating a foreign key constraint and stopping me from inserting into the DataFile table.
I'm having some real trouble with some JPA mappings for a simple #OneToMany mapping.
I'm using EclipseLink and DerbyDB.
#Entity( name = "study2" )
#Access( AccessType.FIELD )
public class Study2 extends EntityBaseItem {
private List<DataFile> datafiles = new ArrayList<DataFile>();
public Study2() { }
#OneToMany( cascade = CascadeType.ALL, orphanRemoval = true )
#JoinColumn( name="STUDY_ID", referencedColumnName = "ID" )
#Access( AccessType.PROPERTY )
public List<DataFile> getDatafiles() {
return this.datafiles;
}
public void setDatafiles( List<DataFile> dfList ) {
this.datafiles = dfList;
}
DataFile.java
#Entity( name = "DataFile" )
public class DataFile extends EntityBaseItem<DataFile> {
private String filename;
private long filesize;
private String fileStatus;
private String fileType;
private String fileSubType;
public DataFile() { }
}
This is my EntityBaseItem.java where the #Id resides:
#MappedSuperclass
public abstract class EntityBaseItem {
#Id
#GeneratedValue( strategy = GenerationType.TABLE )
protected Integer id;
protected EntityBaseItem() {}
public Integer getId() {
return id;
}
public void setId( Integer id ) {
this.id = id;
}
#Override
public int hashCode() {
int hash = 0;
hash += ( this.getId() != null ? this.getId().hashCode() : 0);
return hash;
}
#Override
public boolean equals(Object object) {
if (this == object)
return true;
if (object == null)
return false;
if (getClass() != object.getClass())
return false;
EntityBaseItem other = (EntityBaseItem)object;
if (this.getId() != other.getId() && (this.getId() == null || !this.id.equals(other.id))){
return false;
}
return true;
}
}
The problem is that when I create a Study2 object with some DataFile objects and try to persist it to my DB then I get the error
UPDATE on table 'DATAFILE' caused a violation of foreign key constraint 'DATAFILE_STUDY_ID' for key
If I change the annotation on getDataFiles() and remove the #JoinColumn ( see below ) then the mapping works, however it creates a join table and I'd really rather just have a join column in the DataFile table:
#OneToMany( cascade = CascadeType.ALL, orphanRemoval = true )
#Access( AccessType.PROPERTY )
public List<DataFile> getDatafiles() {
return this.datafiles;
}
I guess it's down to having my #Id in EntityBaseItem as when I removed that and added #Id in the Study2 class then it worked as expected, however there must be some way to keep #Id in the EntityBaseItem and still use a #JoinColumn? I've not had any issues elsewhere in my code, and I have various other mappings which are not as simple as this one.
I know what the error means, however I don't know why it's happening. To me I'd expect my code to work and cascade the DataFiles automatically with a new id for each.
Here is the code that actually causes the error to be thrown:
Study2 testStudy = new Study2();
// set some datafiles etc.
EntityManager em = getEM(); // gives me EntityManager
em.getTransaction().begin();
em.persist( testStudy );
em.getTransaction().commit();
I'd simplified it down to that for testing, throws error on .commit() and then it rolls back the commit.
Change your mappings
public class Study2(){
#OneToMany( cascade = CascadeType.ALL, orphanRemoval = true,mappedBy="study2")
#Access( AccessType.PROPERTY )
public List<DataFile> getDatafiles() {
return this.datafiles;
}
}
Here we say that DataFile is mappedBy "study2" in DataFile class and Study2 has JoinColumn. And the Study2 is inverse side of relationship and will not update the relationship when it gets updated.
Add one field Study2 in DataFile, I have given mapping on field.You can change that
#ManyToOne
#JoinColumn(name="STUDY_ID", referencedColumnName = "ID")
private Study2 study2;
It states that many DataFile are present in one Study2 class
Related
I would like to do the following:
Inserting the CityHistory into the database using JPA.
The first time there is no data, so a new city will be inserted. (IT WORKS FINE)
the (IDENTIFICATION) within the city table is a unique field.
What I want to achieve is when I am inserting the same city again is to reuse the existing field instead of trying to create a new one (identification will be like a city's unique name).
So how can I do that using JPA or Hibernate?
#Entity
public class CityHistory extends History implements Serializable {
#Id
#Column(name = "KEY_CITY_HISTORY", nullable = false, precision = 19)
private Long id;
#ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
#JoinColumn(name = "CITY_ID", nullable = false, foreignKey = #ForeignKey(name = "FK_CITY_ID"))
private City cityId;
#Column(name = "CITY_NAME", nullable = false)
private String cityName;
}
#Entity
public class City implements Serializable {
#Id
#Column(name = "KEY_CITY", nullable = false, precision = 19)
private Long id;
#Column(name = "IDENTIFICATION", nullable = false, unique = true)
private String identification;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "MUNICIPALITY_ID", foreignKey = #ForeignKey(name = "FK_MUNICIPALITY_ID"))
private Municipality municipalityId;
}
UPDATE
Here is how I am writing the data to the database,
It's a Spring Batch itemWriter
#Component
public class InfoItemWriter implements ItemWriter<Object> {
#Autowired
private CityHistoryRepository cityHistoryRepository;
#Override
public void write(List<? extends Object> items) throws Exception {
if (items.size() > 0 && items.get(0) instanceof CityHistory) {
cityHistoryRepository.saveAll((List<? extends CityHistory>) items);
}
}
}
First of all thanks to all who tried to help!
Reading the resources that #Benjamin Maurer provided:
I don't think you want the cascade on the ManyToOne side, see One-To-Many
The most common Parent – Child association consists of a one-to-many and a many-to-one relationship, where the cascade being useful for the one-to-many side only
As the relation I have is ManyToOne it was really not useful to use the cascade and doesn't serve my need.
I used a different approache to reach the goal. I have created a service where it validates the existence of a city, then adds a new city if it does not exist.
#Service
public class CityHistoryServiceImpl implements CityHistoryService {
#Autowired
CityRepository cityRepository;
#Autowired
CityHistoryRepository cityHistoryRepository;
#Override
public Optional<CityHistory> addCityHistory(City city, String cityName, ..) {
if (city != null && cityName != null) {
City city1 = addCityIfNotExist(city);
CityHistory cityHistory = new CityHistory();
cityHistory.setCityId(city1);
cityHistory.setCityName(cityName);
cityHistoryRepository.save(cityHistory);
return Optional.of(cityHistory);
}
return Optional.empty();
} ....
private City addCityIfNotExist(City city) {
City city1 = cityRepository.findFirstByBagId(city.getBagId());
if (city1 == null) {
city1 = cityRepository.save(city);
}
return city1;
}
}
Hibernate will use the #Id property of City to determine if it is new or not. When it is null, Hibernate couldn't possibly know that a similar entry already exists.
So you need to perform a query to find each city first:
for (var history : histories) {
var cities = em.createQuery("select city from City city where city.identification = ?1", City.class)
.setParameter(1, history.getCityId().getIdentification())
.getResultList();
if (!cities.isEmpty()) {
history.setCityId(cities.get(0));
}
em.persist(history);
}
If you use Hibernate and City.identification is unique and always non-null, you can use it as a NaturalID:
In City:
#NaturalId
private String identification;
Then:
for (var history : histories) {
var city = em.unwrap(Session.class)
.byNaturalId(City.class)
.using("identification", history.getCityId().getIdentification())
.getReference();
if (city != null) {
history.setCityId(city);
}
em.persist(history);
}
But if you do have City.id set, i.e., not null, you can use EntityManager.merge to get a managed entity:
for (var history : histories) {
City city = history.getCityId();
if (city.getId() != null) {
city = em.merge(city);
history.setCityId(city);
}
em.persist(history);
}
One more remark: We are not in the relational domain, but we are mapping object graphs. So calling your fields cityId and municipalityId is arguably wrong - even the type says so: City cityId.
They are not just plain identifiers, but full fledged objects: City city.
So my second post. This time i worked on a passion project of mine, which turned out to be far more complicated than I expected and again I need some help.
I have two enitites: Gamestate and User.
Users are supposed to be able to join multiple Games(/gamestates). Games(/gamestates) are supposed to have muliple people join them. So therefore it is represented as a N:M Relation.
Depending on who joins and when they join they are supposed to have different roles, giving them different rights in the app. Which means I needed an N:M Relation with custom fields and therefore I had to model the relation table myself. That's as far as I have come.
Abstract Model:
#EqualsAndHashCode
#Getter
#Setter
#ToString
public abstract class AbstractModel {
#Id
#GeneratedValue
protected Long id;
#NotNull
protected String identifier;
}
User
#Getter
#Setter
#Entity
#Builder
#NoArgsConstructor
#AllArgsConstructor
#ToString(callSuper = true)
#EqualsAndHashCode(callSuper = true)
public class User extends AbstractModel{
private String nickName;
private UserRole role;
#ToString.Exclude
#EqualsAndHashCode.Exclude
#OneToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY, mappedBy = "user", orphanRemoval = true)
private LoginInformation loginInformation;
#ToString.Exclude
#EqualsAndHashCode.Exclude
#OneToMany(cascade = {CascadeType.PERSIST}, fetch = FetchType.LAZY, mappedBy = "gameState")
private List<UserGameState> userGameStates = new ArrayList<>();
//DTO Constructor
public User(UserDTO userDTO){
this.identifier = Optional.ofNullable(userDTO.getIdentifier())
.orElse(UUID.randomUUID().toString());
this.nickName = userDTO.getNickName() == null ? "": userDTO.getNickName();
this.role = UserRole.valueOf(userDTO.getRole());
this.loginInformation = null;
if(userDTO.getLoginInformation() != null) {
setLoginInformation(new LoginInformation(userDTO.getLoginInformation()));
} else {
setLoginInformation(new LoginInformation());
}
(userDTO.getUserGameStates() == null ? new ArrayList<GameStateDTO>() : userDTO.getUserGameStates())
.stream()
.map(x -> new UserGameState((UserGameStateDTO) x))
.forEach(this::addUserGameState);
}
GameState
#Getter
#Setter
#Entity
#Builder
#NoArgsConstructor
#AllArgsConstructor
#ToString(callSuper = true)
#EqualsAndHashCode(callSuper = true)
public class GameState extends AbstractModel{
private String name;
private String description;
private String image;
#ToString.Exclude
#EqualsAndHashCode.Exclude
#OneToMany(cascade = {CascadeType.PERSIST}, fetch = FetchType.LAZY, mappedBy = "user")
private List<UserGameState> userGameStates = new ArrayList<>();
//DTO Constructor
public GameState(GameStateDTO gameStateDTO){
this.identifier = Optional.ofNullable(gameStateDTO.getIdentifier())
.orElse(UUID.randomUUID().toString());
this.name = gameStateDTO.getName() == null ? "": gameStateDTO.getName();
this.description = gameStateDTO.getDescription() == null ? "": gameStateDTO.getDescription();
this.image = gameStateDTO.getImage() == null ? "": gameStateDTO.getImage();
(gameStateDTO.getUserGameStates() == null ? new ArrayList<UserDTO>() : gameStateDTO.getUserGameStates())
.stream()
.map(x -> new UserGameState((UserGameStateDTO) x))
.forEach(this::addUserGameState);
}
//----------------------1:1 Relationship Methods----------------------
//----------------------1:N Relationship Methods----------------------
public void addUserGameState(UserGameState userGameState) {
if (userGameStates.contains(userGameState)) {
return;
}
userGameStates.add(userGameState);
userGameState.setGameState(this);
}
public void removeUserGameState(UserGameState userGameState) {
if (!userGameStates.contains(userGameState)) {
return;
}
userGameState.setGameState(null);
userGameStates.remove(userGameState);
}
//----------------------N:1 Relationship Methods----------------------
//----------------------N:M Relationship Methods----------------------
}
UserGameSatet (Custom N:M Table)
#Getter
#Setter
#Entity
#Builder
#ToString
#NoArgsConstructor
#AllArgsConstructor
#EqualsAndHashCode
public class UserGameState{
#EmbeddedId
private User_GameState_PK id;
#ManyToOne(cascade = {CascadeType.PERSIST}, fetch = FetchType.LAZY)
#MapsId("user_id")
#JoinColumn(name = "USER_ID", insertable = false, updatable = false)
private User user;
#ManyToOne(cascade = {CascadeType.PERSIST}, fetch = FetchType.LAZY)
#MapsId("gameState_id")
#JoinColumn(name = "GAMESTATE_ID", insertable = false, updatable = false)
private GameState gameState;
//add Role later
public UserGameState(User u, GameState gs) {
// create primary key
this.id = new User_GameState_PK(u.getId(), gs.getId());
// initialize attributes
setUser(u);
setGameState(gs);
}
public UserGameState(UserGameStateDTO userGameStateDTO){
//this.id =
this.user = null;
this.gameState = null;
}
//----------------------1:1 Relationship Methods----------------------
//----------------------1:N Relationship Methods----------------------
//----------------------N:1 Relationship Methods----------------------
public void setUser(User user) {
if (Objects.equals(this.user, user)) {
return;
}
User oldUser = this.user;
this.user = user;
if (oldUser != null) {
oldUser.removeUserGameState(this);
}
if (user != null) {
user.addUserGameState(this);
}
}
public void setGameState(GameState gameState) {
if (Objects.equals(this.gameState, gameState)) {
return;
}
GameState oldGameState = this.gameState;
this.gameState = gameState;
if (oldGameState != null) {
oldGameState.removeUserGameState(this);
}
if (oldGameState != null) {
oldGameState.addUserGameState(this);
}
}
//----------------------N:M Relationship Methods----------------------
}
User_GameState_PK (Combined Key)
#Embeddable
#Builder
#ToString
#NoArgsConstructor
#AllArgsConstructor
#Getter
#Setter
public class User_GameState_PK implements Serializable {
#Column(name = "USER_ID")
private Long user_id;
#Column(name = "GAMESTATE_ID")
private Long gameState_id;
public User_GameState_PK(long user_id, long gameState_id){
this.user_id = user_id;
this.gameState_id = gameState_id;
}
#Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass())
return false;
User_GameState_PK that = (User_GameState_PK) o;
return Objects.equals(user_id, that.user_id) &&
Objects.equals(gameState_id, that.gameState_id);
}
#Override
public int hashCode() {
return Objects.hash(user_id, gameState_id);
}
}
The method saving the Connection in my Service
(both GameState and User are already instantiated, and the method gets the identifier of both objects, retrieving them from the database and adding the relation between them.)
public Optional<GameStateDTO> addUserToGameState(String identifierGS, String identifierU) {
GameState gameState = gameStateRepo.findByIdentifier(identifierGS)
.orElseThrow(() -> new IllegalArgumentException("GameState ID has no according GameState."));
User user = userRepo.findByIdentifier(identifierU)
.orElseThrow(() -> new IllegalArgumentException("User ID has no according User."));
//Custom N:M Connection Part
UserGameState connection = new UserGameState(user, gameState);
userGameStateRepo.save(connection);
return Optional.of(gameState)
.map(m -> convertModelIntoDTO(m));
}
I managed to set the N:M table up, together with its combined key. I tested it with simple CRUD Routes, and they worked.
Next I tried to set up some routes so that people could actually join a game(/gamestate) at which point it throws the following exception upon saving.
javax.persistence.EntityExistsException: A different object with the same identifier value was already associated with the session : [com.Astralis.backend.model.UserGameState#User_GameState_PK(user_id=1, gameState_id=7)]
After reading through some posts on stackoverflow I tried out changing the Cascadetype to .MERGE, which resulted in this exception.
javax.persistence.EntityNotFoundException: ...
Really I am lost here, it feels like if I use .PERSIST, Hibernate complaines that it copies itself while saving the Relation. While if I change it to .MERGE, it complaines that the value isn't already present in the first place.
I am more than thankfull for any breadcrumb bringing me closer to a solution, as this turned out to be a gigantic roadblock for the project, and I have tried out everything that I can think of.
So after a few more days of searching I managed to solve it.
For this I first remade a guide's project in with the data structure from the guide and the service/controller structure of my project. Testing if it would work, and as it did I just started comparing the models with each other and tried all different possibilities out, to find out what is actually causing the issues.
The used guide is this one: https://vladmihalcea.com/the-best-way-to-map-a-many-to-many-association-with-extra-columns-when-using-jpa-and-hibernate/
I had six Copy&Paste (kinda) mistakes that caused Hibernate to falsely associate table columns with each other. These were:
in User:
...
#ToString.Exclude
#EqualsAndHashCode.Exclude
#OneToMany(
cascade = {CascadeType.PERSIST},
fetch = FetchType.LAZY,
mappedBy = "user",// changed from gameState to user
orphanRemoval = true
)
private List<UserGameState> userGameStates = new ArrayList<>();
...
in GameState the reverse:
...
#ToString.Exclude
#EqualsAndHashCode.Exclude
#OneToMany(cascade = {CascadeType.PERSIST},
fetch = FetchType.LAZY,
mappedBy = "gameState",// changed from user to gameState
orphanRemoval = true)
private List<UserGameState> userGameStates = new ArrayList<>();
...
3&4. The JoinColumn Annotations were unnecessary, seemingly I combiend multiple guides into one project. This caused then even more issues:
...
#ManyToOne(
cascade = {CascadeType.PERSIST},
fetch = FetchType.LAZY)
#MapsId("user_id")
//#JoinColumn(name = "USER_ID", insertable = false, updatable = false) //this one removed
private User user;
#ManyToOne(
cascade = {CascadeType.PERSIST},
fetch = FetchType.LAZY)
#MapsId("gameState_id")
//#JoinColumn(name = "GAMESTATE_ID", insertable = false, updatable = false) //this one removed
private GameState gameState;
...
5&6. Two minor copy&paste mistakes, in the "continuity keeper" methods in UserGameState:
...
public void setGameState(GameState gameState) {
if (Objects.equals(this.gameState, gameState)) {
return;
}
GameState oldGameState = this.gameState;
this.gameState = gameState;
if (oldGameState != null) {
oldGameState.removeUserGameState(this);
}
//I copied the previous if block, and replaced the remove... with add...
//But I didn't change the oldGameState to gameState.
//This didn't throw any errors, and actually it still created the relations properly, but I am pretty sure it would cause issues further down the line.
if (gameState != null) {
gameState.addUserGameState(this);
}
}
...
So how does this work now:
As before, when the route with the Identifiers for the connected GameState and User is called, the service "addUserToGameState" is called, getting the models with the given Identifiers.
...
public Optional<GameStateDTO> addUserToGameState(String identifierGS, String identifierU) {
GameState gameState = gameStateRepo.findByIdentifier(identifierGS)
.orElseThrow(() -> new IllegalArgumentException("GameState ID has no according GameState."));
User user = userRepo.findByIdentifier(identifierU)
.orElseThrow(() -> new IllegalArgumentException("User ID has no according User."));
//Custom N:M Connection Part
UserGameState connection = new UserGameState(user, gameState);
return Optional.of(gameState)
.map(m -> convertModelIntoDTO(m));
}
...
After that the UserGameState cosntructer is called, which sets and creates the combined key and calls the setter methods for the related User/GameState fields.
...
public UserGameState(User u, GameState gs) {
// create primary key
this.id = new User_GameState_PK(u.getId(), gs.getId());
// initialize attributes
setUser(u);
setGameState(gs);
}
...
I wrote the setters in a way, that they at the same time, check the added models for relationship consistency issues, and adjust their fields according to if they are newly edited or replaced.
...
public void setUser(User user) {
if (Objects.equals(this.user, user)) {
return;
}
User oldUser = this.user;
this.user = user;
if (oldUser != null) {
oldUser.removeUserGameState(this);
}
if (user != null) {
user.addUserGameState(this);
}
}
public void setGameState(GameState gameState) {
if (Objects.equals(this.gameState, gameState)) {
return;
}
GameState oldGameState = this.gameState;
this.gameState = gameState;
if (oldGameState != null) {
oldGameState.removeUserGameState(this);
}
if (gameState != null) {//copy paste error
gameState.addUserGameState(this);
}
}
...
I have 1:m relation. I post data about "1" and also about "m" relation in one post. What i am trying to achieve is to insert data ( m ) into "1" , then persist 1 into database which should create info in database about 1 and about m.
The "1" Enitity:
private List<OptionEntity> options;
#OneToMany(mappedBy = "survey", cascade = CascadeType.MERGE)
public List<OptionEntity> getOptions() {
return options;
}
public void setOptions(List<OptionEntity> options) {
this.options= options;
}
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "survey_id", nullable = false)
public int getSurveyId() {
return surveyId;
}
public void setSurveyId(int surveyId) {
this.surveyId = surveyId;
}
the "m" entitites
private SurveyEntity survey;
#ManyToOne(fetch=FetchType.LAZY)
#JoinColumn(name="survey_id")
public SurveyEntity getSurvey() {
return survey;
}
public void setSurvey(SurveyEntity survey ) {
this.survey = survey;
}
#Id
#GeneratedValue(strategy=GenerationType.AUTO)
#Column(name = "option_id", nullable = false)
public int getOptionId() {
return optionId;
}
public void setOptionId(int optionId) {
this.optionId = optionId;
}
However when i do
List<OptionEntity> ops = new ArrayList<>();
for( String option : options ){
OptionEntity tmp_option = new OptionEntity();
tmp_option.setText( option );
ops.add(tmp_option);
}
survey.setOptions(ops);
surveyDAO.add(survey);
when add is
public void add ( SurveyEntity s )
{
em.persist( s );
}
Creates only record for "1" entity in database. The records for all "m" entities are not inserted in the databases.
I thought whats important here is identity set to AUTO for m entities so database can create their id ( it has autoincrement ).
Seems i am wrong in this one.
What is the correct way to insert into 1:m relation at once?
Thanks for help
You have to do two things:
1) Set the relationship on both sides, so in the loop add the Survey entity to each of the Option entities:
for( String option : options ){
OptionEntity tmp_option = new OptionEntity();
tmp_option.setText( option );
ops.add(tmp_option);
tmp_option.setSurvey(survey);
}
2) Either use em.merge() instead of em.persist() or add this cascade option:
#OneToMany(mappedBy = "survey", cascade = {CascadeType.MERGE, CascadeType.PERSIST})
public List<OptionEntity> getOptions() {
return options;
}
I want to save/persist an entity(parent) with unique children objects included only by the parent object.
Anything works well until a duplicate child appears, here I get following Exception:
Caused by: com.mysql.jdbc.exceptions.jdbc4.MySQLIntegrityConstraintViolationException: Duplicate entry
At first you have to know that I'm using the expression session.bySimpleNaturalId(Child.class).load(child.getMd5Hash()) to check if the child already exists, because all children objects have unique hash values(created after initializing) which are explicitly not assigned as the primary key(their primary key is an auto-increment; strategy = GenerationType.TABLE).
Whether I use session.merge(child) or any other expression on my DAO, I get the same exception.
My Parent-Object:
#Entity
#Table(name = "parent")
public class Parent implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.TABLE)
private Long id = null;
#OneToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
#JoinColumn(name = "kind_child_id", referencedColumnName = "md5hash")
private Child firstChild;
#OneToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
#JoinColumn(name = "rude_child_id", referencedColumnName = "md5hash")
private Child secondChild;
//private String attributes;
//Getters & Setters
My Child-Object:
#Entity
#Table(name = "child")
public class Child implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.TABLE)
private Long id = null;
#NaturalId
#Column(name="md5hash", unique=true)
private char[] md5hash = new char[32];
//private String attributes;
//Getters & Setters
And here is my method to save/persist a parent(validations included):
public void writeParent(Parent parent) {
try {
if (parent != null) {
if (globalSession == null) {
globalSession = getSessionFactory().openSession();
}
globalSession.beginTransaction();
if (parent.getFirstChild() != null) {
Child tempFirstChild = (Child) globalSession.bySimpleNaturalId(Child.class).load(parent.getFirstChild().getMd5Hash());
if (tempFirstChild != null) {
parent.setFirstChild(tempFirstChild);
//globalSession.merge(tempFirstChild);
//throws MySQLICV-Exception
//globalSession.update(tempFirstChild);
//throws MySQLICV-Exception
}
}
if (parent.getSecondChild() != null) {
Child tempSecondChild = (Child) globalSession.bySimpleNaturalId(Child.class).load(parent.getSecondChild().getMd5Hash());
if (tempSecondChild != null) {
parent.setSecondChild(tempSecondChild);
//globalSession.merge(tempSecondChild);
//throws MySQLICV-Exception
//globalSession.update(tempSecondChild);
//throws MySQLICV-Exception
}
}
globalSession.saveOrUpdate(parent);
//globalSession.persist(parent);
globalSession.getTransaction().commit();
}
} catch (Exception ex) {
log.error("FAILURE: ", ex);
}
finally{
globalSession.close();
}
}
Maybe I didn't understand the entire documentary that's why I came up for this: Do I even have to tell Hibernate to merge the found entities?
How can I tell Hibernate that those children shouldn't be treated as new objects which need to be persisted?
Or do I even have to switch to bidirectional relations?(I'm currently not allowed to use bidirectional relations in this case)
Any hints are welcome and very appreciated, thank you in advance.
Regards, Yeti
I found the answer a.k.a the mistake myself.
At first to fix it I used a separate function to check if a child exists, furthermore I used separate sessions to check them(maybe it doesn't work if L2C is disabled).
synchronized public Child checkChild(Child child) {
try {
if (child != null) {
tempSession = getSessionFactory().openSession();
Child tempChild = (Child) tempSession.bySimpleNaturalId(Child.class).load(Child.getMd5Hash());
if (tempChild != null) {
return tempChild;
} else {
return child;
}
} else {
return null;
}
} catch (Exception ex) {
return null;
}
finally{
tempSession.close();
}
}
At second and there is the clue, I tried to save the parent object as the parametrical value, which is wrong, so just initiate a new parent-object within the method and it's done.
public void writeParent(Parent parent) {
tempParent = new Parent();
tempParent.setFirstChild(checkChild(tempParent.getFirstChild()));
tempParent.setSecondChild(checkChild(tempParent.getSecondChild()));
globalSession = getSessionFactory().openSession();
globalSession.beginTransaction();
globalSession.saveOrUpdate(tempParent);
globalSession.getTransaction().commit();
globalSession.close();
}
My Hibernate-JPA domain model has these entities:
AttributeType ------< AttributeValue
The relevant Java classes look like this (getters and setters omitted):
#Entity
public class AttributeType {
#Id #GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
#Column(unique = true, nullable = false)
private String name;
#OneToMany(mappedBy = "attributeType", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.EAGER)
private List<AttributeValue> values = new ArrayList<AttributeValue>();
}
#Entity #Table(uniqueConstraints = #UniqueConstraint(columnNames = {"value", "attribute_type_id"}))
public class AttributeValue {
#Id #GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
#ManyToOne(optional = false)
private AttributeType attributeType;
#Column(nullable = false)
private String value;
}
Notice there's a unique constraint on AttributeValue.value and AttributeValue.attributeType, because for an attribute type (e.g. size) we don't want to allow an attribute value (e.g. small) to occur more than once.
If I update an AttributeType by performing the following operations within a single transaction:
delete "small" attribute value from "size" attribute type
add "small" attribute value to "size" attribute type
I get an exception that indicates the unique constraint was violated. This suggests that Hibernate-JPA is performing the insertion of the attribute value before the delete, which seems to invite this kind of problem for no obvious reason.
The class that performs the update of an AttributeType looks like this:
#Transactional(propagation = Propagation.SUPPORTS)
public class SomeService {
private EntityManager entityManager; // set by dependency injection
#Transactional(propagation = Propagation.REQUIRED)
public AttributeType updateAttributeType(AttributeType attributeType) throws Exception {
attributeType = entityManager.merge(attributeType);
entityManager.flush();
entityManager.refresh(attributeType);
return attributeType;
}
}
I could workaround this problem by iterating over the attribute values, figuring out which ones have been updated/deleted/inserted, and performing them in this order instead:
deletes
updates
inserts
But it seems like the ORM should be able to do this for me. I've read that Oracle provides a "deferConstraints" option that causes constraints to be checked only when a transaction has completed. However, I'm using SQL Server, so this won't help me.
You need to use a composite ID instead of a generated ID.
HHH-2801
The problem arises when a new association entity with a generated ID
is added to the collection. The first step, when merging an entity
containing this collection, is to cascade save the new association
entity. The cascade must occur before other changes to the collection.
Because the unique key for this new association entity is the same as
an entity that is already persisted, a ConstraintViolationException is
thrown. This is expected behavior.
Using a new collection (i.e., one-shot delete), as suggested in the
previous comment) also results in a constraint violation, since the
new association entity will be saved on the cascade of the new
collection.
An example of one of the approaches (using a composite ID instead of a generated ID) is illustrated >in manytomanywithassocclass.tar.gz and is checked into Svn.
#Entity
public class AttributeType {
#Id
#GeneratedValue(strategy = GenerationType.SEQUENCE)
private Integer id;
#Column(unique = true, nullable = false)
private String name;
#OneToMany(mappedBy = "attributeType", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.EAGER)
private List<AttributeValue> values = new ArrayList<AttributeValue>();
//Getter, Setter...
}
#Entity
#Table (uniqueConstraints = #UniqueConstraint(columnNames = { "value", "attributeType_id" }))
public class AttributeValue{
#EmbeddedId AttributeValueId id;
#MapsId(value= "id")
#ManyToOne(optional = false)
private AttributeType attributeType;
private String value2;
public AttributeValue() {
this.id = new AttributeValueId();
}
public AttributeType getAttributeType() {
return attributeType;
}
public void setAttributeType(AttributeType pAttributeType) {
this.id.setAttributeTypeID(pAttributeType.getId());
this.attributeType = pAttributeType;
}
public String getValue() {
return id.getAttributeValue();
}
public void setValue(String value) {
this.id.setAttributeValue(value);
}
#Embeddable
public static class AttributeValueId implements Serializable {
private Integer id;
private String value;
public AttributeValueId() {
}
public AttributeValueId(Integer pAttributeTypeID, String pAttributeValue) {
this.id = pAttributeTypeID;
this.value = pAttributeValue;
}
public Integer getAttributeTypeID() {
return id;
}
public void setAttributeTypeID(Integer attributeTypeID) {
this.id = attributeTypeID;
}
public String getAttributeValue() {
return value;
}
public void setAttributeValue(String attributeValue) {
this.value = attributeValue;
}
#Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime
* result
+ ((id == null) ? 0 : id
.hashCode());
result = prime
* result
+ ((value == null) ? 0 : value.hashCode());
return result;
}
#Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
AttributeValueId other = (AttributeValueId) obj;
if (id == null) {
if (other.id != null)
return false;
} else if (!id.equals(other.id))
return false;
if (value == null) {
if (other.value != null)
return false;
} else if (!value.equals(other.value))
return false;
return true;
}
}
}
See 5.1.2.1. Composite identifier on how to do it with JPA annotation.
See Chapter 8. Component Mapping
See 8.4. Components as composite identifiers
I am not sure if I understand the question as it is getting late, but first thing I would try would be to override AttributeValue's equals method to contain those two unique fields.
In hibernate session there is one queue for the delete and one for the insert. Debug to see if deletes comes before insert.
Look at the merge. Try using update instead.