Jpa Criteria API, Dynamic Predicates with #OneTomany relation - java

Here are my Entities.
TradeItem.class
#Entity
#Table(name = "TRADEITEMS")
public class TradeItem {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#Column(name = "TRADEITEM_ID")
private Long id;
private String shopOwner;
private Boolean corrupted;
private String base;
private String type;
#OneToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL, orphanRemoval = true)
private Set<Mod> mod;
private String league;
...
}
Mod.class
#Entity
#Table(name = "MODS")
public class Mod {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
private Double miniValue;
private Double maxiValue;
private String modName;
...
}
DaoImpl
#Repository
public class TradeItemDaoImp implements TradeItemDao{
#PersistenceContext
private EntityManager em;
#Override
public Optional<List<TradeItem>> search(TradeItemRequest request) {
CriteriaBuilder criteriaBuilder = em.getCriteriaBuilder();
CriteriaQuery<TradeItem> cq = criteriaBuilder.createQuery(TradeItem.class);
Root<TradeItem> root = cq.from(TradeItem.class);
List<Predicate> predicates = new LinkedList<>();
predicates.add(criteriaBuilder.equal(root.get("league"), request.getLeague()));
if(request.getName() != null){
predicates.add(criteriaBuilder.like(root.get("name"), "%"+request.getName()+"%"));
}
if(request.getType() != null){
predicates.add(criteriaBuilder.equal(root.get("type"), request.getType()));
}
if(request.getBase() != null) {
predicates.add(criteriaBuilder.equal(root.get("base"), request.getBase()));
}
if(request.getIdentified() != null){
predicates.add(criteriaBuilder.equal(root.get("identified"), request.getIdentified()));
}
if(request.getCorrupted() != null){
predicates.add(criteriaBuilder.equal(root.get("corrupted"), request.getCorrupted()));
}
if(request.getMods().size() > 0) {
Join<TradeItem, Mod> mod = root.joinSet("mod");
for (ModRequest m : request.getMods()) {
predicates.add(
criteriaBuilder.and(
criteriaBuilder.equal(mod.get("modName"), m.getName()),
criteriaBuilder.ge(mod.get("miniValue"), m.getMinValue() == null ? 0 : m.getMinValue()),
criteriaBuilder.le(mod.get("maxiValue"), m.getMaxValue() == null ? 10000 : m.getMaxValue()))
);
}
}
cq.select(root)
.where(predicates.toArray(new Predicate[predicates.size()]));
TypedQuery<TradeItem> typedQuery = em.createQuery(cq);
return Optional.of(
typedQuery
.setFirstResult(0)
.setMaxResults(50)
.getResultList());
}
}
Basicly what i am trying to do here is that i have developed a Webscraper for a specific Tradingforum. I have a webapplication where you can search for forum-items through a form. Now each TradingItem contains a set of mods(#OneToMany) which have different names and values.
When i search for an item with only one or zero mods, it works fine. But as soon as i add two or more mods in the search-form, it returns an empty list.
I'm new to the Criteria Api and there is obviously something wrong with my mod-predicates/logic.
I'm using hibernate 5.1.0 & MySql
Example of form
Edit: Solved
I simply had to move the Join to inside the for-loop.
for (ModRequest m : request.getMods()) {
Join<TradeItem, Mod> mod = root.joinSet("mod");
predicates.add(
criteriaBuilder.and(
criteriaBuilder.equal(mod.get("modName"), m.getName()),
criteriaBuilder.ge(mod.get("miniValue"), m.getMinValue() == null ? 0 : m.getMinValue()),
criteriaBuilder.le(mod.get("maxiValue"), m.getMaxValue() == null ? 10000 : m.getMaxValue()))
);
}

Related

Reuse an existing id if exist in the database

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.

Spring Hibernate: EntityExistsException when storing n:m table value with combiend Priamry Key

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);
}
}
...

How to define multiple Join on condition using ctiteria API spring data JPA Specification?

Below is the code for fetch join
public static Specification<Item> findByCustomer(User user) {
return (root, criteriaQuery, criteriaBuilder) -> {
root.fetch(User_.address, JoinType.LEFT);
return criteriaBuilder.equal(root.get(User_.id), 1);
};
}
Above code is generating below query
select us.ALL_COLUMN, adr.ALL_COLUMN from User us left outer join Address adr on us.id=
adr.id where us.id = 1;
How can I add multiple condition on left outer join. I want to generate below query using spring data jpa Specification.
Select us.ALL_COLUMN, adr.ALL_COLUMN from User us left outer join Address adr on us.id=
adr.id and adr.effect_end_date = null where us.id = 1
public static Specification<A> findAbyBname(String input) {
return new Specification<A>() {
#Override
public Predicate toPredicate(Root<A> root, CriteriaQuery<?> cq, CriteriaBuilder cb) {
cq.distinct(true);
Join<A,AB> AjoinAB = root.joinList(A_.AB_LIST,JoinType.LEFT);
Join<AB,B> ABjoinB = AjoinAB.join(AB_.B,JoinType.LEFT);
return cb.equal(ABjoinB.get(B_.NAME),input);
}
};
}
I have something like this. Here are entities:
#Entity
public class A {
#Id
private Long id;
private String name;
#OneToMany(mappedBy = "a")
private List<AB> abList;
}
#Entity
public class B {
#Id
private Long id;
private String name;
#OneToMany(mappedBy = "b")
private List<AB> abList;
}
#Entity
public class AB {
#Id
private Long id;
private String name;
#ManyToOne
#JoinColumn(name = "a_id")
private A a;
#ManyToOne
#JoinColumn(name = "b_id")
private B b;
}
Below way we can do we should have two predcit and then and for those two perdicate , didnt tested
public static Specification<Item> findByCustomer(User user) {
return (root, criteriaQuery, criteriaBuilder) -> {
root.fetch(User_.address, JoinType.LEFT);
Predicate predicateForEndDate
= criteriaBuilder.equal(root.get(User_.end_date), null);
Predicate predicateForUser
= criteriaBuilder.equal(root.get(User_.id), 1);
return criteriaBuilder.and(predicateForEndDate, predicateForUser)
};
}

java.lang.ClassCastException using JPA Queries

I'm trying to create Java EE web aplication but I have problem getting single result from database. Getting list of results isn't a problem.
public UserCredentialsDTO findByUsernameAndPassword(String username, String password) {
EntityManager em = getEntityManager();
TypedQuery<UserCredentialsDTO> q = em.createNamedQuery("UserCredentialsDTO.findByUsernameAndPassword", UserCredentialsDTO.class);
//Query q = em.createNamedQuery("UserCredentialsDTO.findByUsernameAndPassword", UserCredentialsDTO.class);
q.setParameter("un", username);
q.setParameter("pw", password);
UserCredentialsDTO r = null;
try{
r = q.getSingleResult(); //This line is a problem
//r = (UserCredentialsDTO)q.getSingleResult();
} catch(javax.persistence.NoResultException e) {
}
return r;
}
Using both Query and TypedQuery throws java.lang.ClassCastException
java.lang.ClassCastException: wipb.jee.clientdemo.model.UserCredentialsDTO cannot be cast to wipb.jee.clientdemo.model.UserCredentialsDTO
EDIT
UserCredentialsDTO:
#NamedQueries(
{#NamedQuery(name = "UserCredentialsDTO.findByUsernameAndPassword", query = "select uc from UserCredentialsDTO uc where uc.username=:un and uc.password=:pw")}
)
#Entity
#Table(name="USERCREDENTIALS", schema="APP")
public class UserCredentialsDTO implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String username;
private String password;
#OneToMany(mappedBy = "userCredentials", cascade = CascadeType.ALL, fetch = FetchType.EAGER, orphanRemoval = true)
private List<UserGroupDTO> userGroups = new LinkedList<>();
//getters and setters
public void add(UserGroupDTO userGroup) {
userGroup.setUserCredentials(this);
this.userGroups.add(userGroup);
}
public List<UserGroupDTO> getUserGroups() {
return userGroups;
}
}
This looks like an environment issue, where different classloaders are being used for JPA and the rest of the application.
Refer to this thread

error updating parent-child relationship using Hibernate-JPA

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.

Categories