I'm trying to make sure that a model is not persisted twice in the database and its id is symmetrical. Under symmetrical composite id I mean the following:
#Entity
public class Item {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "item_id", unique = true, nullable = false)
public Long id;
// other properties ...
}
#Entity
public class Pair {
#EmbeddedId
public PairId id;
// other properties...
#Embeddable
public static class PairId implements Serializable {
#ManyToOne(cascade={CascadeType.MERGE, CascadeType.REFRESH, CascadeType.DETACH})
#JoinColumn(name="source_item_id")
public Item source;
#ManyToOne(cascade={CascadeType.MERGE, CascadeType.REFRESH, CascadeType.DETACH})
#JoinColumn(name="target_item_id")
public Item target;
#Override
public boolean equals(Object o) {
if(this == o){
return true;
}
if (o instanceof PairId == false){
return false;
}
PairId other = (PairId) o;
return (this.source.equals(other.source) && this.target.equals(other.target)) ||
(this.source.equals(other.target) && this.target.equals(other.source));
}
#Override
public int hashCode() { //probably not the best approach
return source.hashCode() + target.hashCode();
}
}
}
Example:
Item i1 = new Item();
Item i2 = new Item();
//persist items into the database ...
PairId pId1 = new PairId(i1, i2);
PairId pId2 = new PairId(i2, i1);
Pair p1 = new Pair(pId1);
//persist p1 into the database
Pair p2 = new Pair(pId2);
//calling persist should not add new entry to the database, since p2 is symmetrical to p1 and already exists in the database
Pair p3 = findById(pId2);
//p3 should now contain p1 also
Do you have any idea how I could implement such a behaviour? Thanks in advance!
Edit:
Added comments on both classes in order to show that those classes could have(and they have) other properties except of the listed ids above. But for the sake of simplicity I just left their ids as alone standing property.
First of all, I would not use a composite ID. Use an autogenerated surrogate key, and store both items as regular properties.
Then, when storing the items in a pair, I would just make sure to always store them in the same order. For example, the source ID should always be smaler than the target ID. This can be ensured using encapsulation:
public void setItems(Item i1, Item i2) {
if (i1.getId().compareTo(i2.getId()) < 0) {
this.source = i1;
this.target = i2;
}
else {
this.source = i2;
this.target = i1;
}
}
Maybe, just maybe you could drop the Pair idea and use a self reference in Item. This way you have less tables, cleaner code and no composite keys.
Code:
#Entity
public class Item
{
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "id")
private Long id;
#OneToOne
#JoinColumn(name = "source_id", referencedColumnName = "id")
private Item source;
#OneToOne(mappedBy = "source")
private Item target;
}
Related
I am trying to store a class named Project with the following attributes:
#Entity
public class Project implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
// Some more attributes...
#OneToOne(cascade = CascadeType.ALL)
#JoinColumn(name = "projectServices_id", referencedColumnName = "id")
private ProjectServices projectServices;
#OneToOne(cascade = CascadeType.ALL)
#JoinColumn(name = "projectCategories_id", referencedColumnName = "id")
private ProjectCategories projectCategories;
// Constructors
// Getters and Setters
}
The projectCategories and projectServices look very similar, because they both only store lots of boolean variables.
They do look something like this:
#Entity
public class ProjectCategories implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#JsonIgnore
private Long id;
#JsonIgnore
#OneToOne(mappedBy = "projectCategories", cascade = CascadeType.ALL)
private Project project;
private boolean category0;
// Categories 1 - 244
private boolean category245;
// Constructors
// Getters and Setters
#JsonIgnore
#IgnoreForBinding
public List<Boolean> getBooleanData() {
List<Boolean> data = new ArrayList<Boolean>();
Field[] fields = this.getClass().getDeclaredFields();
for (Field field : fields) {
try {
if (field.getType() == boolean.class) {
data.add((Boolean) field.get(this));
}
} catch (Exception ignored) {
}
}
return data;
}
#JsonIgnore
#IgnoreForBinding
public void setBooleanData(List<Boolean> data) {
Field[] fields = this.getClass().getDeclaredFields();
for (Field field : fields) {
try {
if (field.getType() == boolean.class && !data.isEmpty()) {
field.setAccessible(true);
boolean a = data.get(0);
//Boolean a = Boolean.valueOf();
field.setBoolean(this, a);
data.remove(0);
} else if (field.getType() == boolean.class) {
field.setBoolean(this, false);
}
} catch (Exception ignored) {
}
}
}
}
Problem: When I want to send a Project to the frontend it creates a JSON Object with a List of booleans for the type ProjectServices and a Object for the type ProjectCategories. I want both of them to be a List of booleans.
The two classes ProjectServices and ProjectCategories are basically the same and one could argue to simply use the one which is converted to a List of booleans, but the structure of both of the classes will change in the future. Therefore simply using one class is not the desired solution.
Thank you for your help!
Cheers, nicklas
So, #PeterMmm was right. Using DTOs did solve the problem and is the correct way to do it.
Another workaround was using an additional getter which returns the List and use #JsonIgnore on the actual getter.
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);
}
}
...
This is my first post here, I've been searching for a long time here but I didn't found a problem that seemed similar.
When I use JpaRepository function findOne(id) for one of my classes, it returns null. As if no row had been found for this id.
Of course the database row with this id exists.
Also my class mapping seems right.
I don't understand because I already used findOne() for other classes and I never had any problem.
Anyone can tell me what can be the source of this problem, please ? That would be nice !
This is my DAO :
#Transactional
public interface OrderDetailDAO extends JpaRepository<OrderDetail, Integer>
{
}
This is my Model :
#Entity
#Table(name = "order_detail", schema = "", catalog = AppConfig.databaseSchema)
public class OrderDetail implements Serializable {
private int idOrderDetail;
private Order order;
private Preorder preorder;
private UnitType unitType;
private Sale sale;
private DeliveryStatusType deliveryStatusType;
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "id_Order_Detail")
public int getIdOrderDetail() {
return idOrderDetail;
}
public void setIdOrderDetail(int idOrderDetail) {
this.idOrderDetail = idOrderDetail;
}
#ManyToOne
#JoinColumn(name = "id_Order", referencedColumnName = "id_Order", nullable = false)
public Order getOrder() {
return order;
}
public void setOrder(Order order) {
this.order = order;
}
#ManyToOne
#JoinColumn(name = "id_Preorder", referencedColumnName = "id_Preorder", nullable = false)
public Preorder getPreorder() {
return preorder;
}
public void setPreorder(Preorder preorder) {
this.preorder = preorder;
}
#ManyToOne
#JoinColumn(name = "id_Unit_Type", referencedColumnName = "id_Unit_Type")
public UnitType getUnitType() {
return unitType;
}
public void setUnitType(UnitType unitType) {
this.unitType = unitType;
}
#ManyToOne
#JoinColumn(name = "id_Sale", referencedColumnName = "id_Sale")
public Sale getSale() {
return sale;
}
public void setSale(Sale sale) {
this.sale = sale;
}
#ManyToOne
#JoinColumn(name = "id_Delivery_Status_Type", referencedColumnName = "id_Delivery_Status_Type")
public DeliveryStatusType getDeliveryStatusType() {
return deliveryStatusType;
}
public void setDeliveryStatusType(DeliveryStatusType deliveryStatusType) {
this.deliveryStatusType = deliveryStatusType;
}
}
When I write a request manually, like this :
#Query("SELECT o FROM OrderDetail o WHERE o.idOrderDetail = :idOrderDetail")
public OrderDetail findOneCustom(#Param("idOrderDetail") Integer idOrderDetail);
That works, but that's ugly so I would prefer to use JpaRepository native function findOne()
After all investigation, I have found an interesting answer that is worked for me. I think it is all about defining column type on Db. For my case, I have defined the variable (rid as column) as varchar2(18) that was RID CHAR(18 BYTE).
Java part:
if (dhFlightRepo.findOneFlight(dhFlight.getRid())== null) {
dhFlightRepo.save(dhFlight);
}
If your value that you used as a parameter for findOne() is smallest than set value on column (18 for my case),the jpa doesn't accept value and returns null.You have to change column type as varchar2(18) it can be changeable according to given value on findOne() and work perfect.
I hope that works for all of you.I kindly request to give more detail If someone knows the reason with more detail.
Config
EcliplseLink 2.3.2
JPA 2.0
The entities are auto created from the db schema from netbeans with Entity Classes from Database... wizard.
The controller classes are auto created from netbeans with JPA Controller Classes from Entity Classes... wizard
Short version of question
In a classic scenario, two tables with one to many relation. I create the parent entity, then the child entity and I attach the child to the parent's collection. When I create (controller method) the parent entity, I expect the child entity to be created to and associated with parent. Why doesn't it happen?
Long version
Parent class
#Entity
#XmlRootElement
public class Device implements Serializable {
private static final long serialVersionUID = 1L;
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Basic(optional = false)
private Integer id;
#Column(unique=true)
private String name;
#Temporal(TemporalType.TIMESTAMP)
private Date updated;
#OneToMany(cascade = CascadeType.ALL, mappedBy = "deviceId")
private Collection<NetworkInterface> networkInterfaceCollection;
public Device() {
}
public Device(String name) {
this.name = name;
updated = new Date();
}
// setters and getters...
#XmlTransient
public Collection<NetworkInterface> getNetworkInterfaceCollection() {
return networkInterfaceCollection;
}
public void setNetworkInterfaceCollection(Collection<NetworkInterface> networkInterfaceCollection) {
this.networkInterfaceCollection = networkInterfaceCollection;
}
public void addNetworkInterface(NetworkInterface net) {
this.networkInterfaceCollection.add(net);
}
public void removeNetworkInterface(NetworkInterface net) {
this.networkInterfaceCollection.remove(net);
}
// other methods
}
Child class
#Entity
#Table(name = "NETWORK_INTERFACE")
#XmlRootElement
public class NetworkInterface implements Serializable {
private static final long serialVersionUID = 1L;
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Basic(optional = false)
private Integer id;
private String name;
#Temporal(TemporalType.TIMESTAMP)
private Date updated;
#JoinColumn(name = "DEVICE_ID", referencedColumnName = "ID")
#ManyToOne(optional = false)
private Device deviceId;
public NetworkInterface() {
}
public NetworkInterface(String name) {
this.name = name;
this.updated = new Date();
}
// setter and getter methods...
public Device getDeviceId() {
return deviceId;
}
public void setDeviceId(Device deviceId) {
this.deviceId = deviceId;
}
}
Main class
public class Main {
public static void main(String[] args) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("wifi-dbPU");
DeviceJpaController deviceController = new DeviceJpaController(emf);
NetworkInterfaceJpaController netController = new NetworkInterfaceJpaController(emf);
Device device = new Device("laptop");
NetworkInterface net = new NetworkInterface("eth0");
device.getNetworkInterfaceCollection().add(net);
deviceController.create(device);
}
}
This class throws a NullPointerException in line: device.getNetworkInterfaceCollection().add(net);
The system knows that there is a new entity device and it has an element net in it's collection. I expected it to write device in db, get device's id, attach it to net and write it in db.
Instead of this, I found that these are the steps I have to do:
deviceController.create(device);
net.setDeviceId(device);
device.getNetworkInterfaceCollection().add(net);
netController.create(net);
Why do I have to create the child when the parent class knows it's child and it should create it for me?
The create method from DeviceJpaController (sorry for the long names in fields, they are auto generated).
public EntityManager getEntityManager() {
return emf.createEntityManager();
}
public void create(Device device) {
if (device.getNetworkInterfaceCollection() == null) {
device.setNetworkInterfaceCollection(new ArrayList<NetworkInterface>());
}
EntityManager em = null;
try {
em = getEntityManager();
em.getTransaction().begin();
Collection<NetworkInterface> attachedNetworkInterfaceCollection = new ArrayList<NetworkInterface>();
for (NetworkInterface networkInterfaceCollectionNetworkInterfaceToAttach : device.getNetworkInterfaceCollection()) {
networkInterfaceCollectionNetworkInterfaceToAttach = em.getReference(networkInterfaceCollectionNetworkInterfaceToAttach.getClass(), networkInterfaceCollectionNetworkInterfaceToAttach.getId());
attachedNetworkInterfaceCollection.add(networkInterfaceCollectionNetworkInterfaceToAttach);
}
device.setNetworkInterfaceCollection(attachedNetworkInterfaceCollection);
em.persist(device);
for (NetworkInterface networkInterfaceCollectionNetworkInterface : device.getNetworkInterfaceCollection()) {
Device oldDeviceIdOfNetworkInterfaceCollectionNetworkInterface = networkInterfaceCollectionNetworkInterface.getDeviceId();
networkInterfaceCollectionNetworkInterface.setDeviceId(device);
networkInterfaceCollectionNetworkInterface = em.merge(networkInterfaceCollectionNetworkInterface);
if (oldDeviceIdOfNetworkInterfaceCollectionNetworkInterface != null) {
oldDeviceIdOfNetworkInterfaceCollectionNetworkInterface.getNetworkInterfaceCollection().remove(networkInterfaceCollectionNetworkInterface);
oldDeviceIdOfNetworkInterfaceCollectionNetworkInterface = em.merge(oldDeviceIdOfNetworkInterfaceCollectionNetworkInterface);
}
}
em.getTransaction().commit();
} finally {
if (em != null) {
em.close();
}
}
}
I finally understood the logic behind persisting one to many entities. The process is:
Create parent class
Persist it
Create child class
Associate child with parent
Persist child (the parent collection is updated)
With code:
public class Main {
public static void main(String[] args) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("wifi-dbPU");
DeviceJpaController deviceController = new DeviceJpaController(emf);
NetworkInterfaceJpaController netController = new NetworkInterfaceJpaController(emf);
Device device = new Device("laptop"); // 1
deviceController.create(device); // 2
NetworkInterface net = new NetworkInterface("eth0"); // 3
net.setDeviceId(device.getId()); // 4
netController.create(net); // 5
// The parent collection is updated by the above create
}
}
Now, I can find a device (with id for example) and I can get all its children using
Collection<NetworkInterface> netCollection = device.getNetworkInterfaceCollection()
In the device entity class, which I posted in the question, there is no need for the methods addNetworkInterface and removeNetwokrInterface.
#Dima K is correct in what they say. When you do this:
Device device = new Device("laptop");
NetworkInterface net = new NetworkInterface("eth0");
device.getNetworkInterfaceCollection().add(net);
deviceController.create(device);
The collection in device hasn't been initialized and so you get a NPE when trying to add to it. In your Device class, when declaring your Collection, you can also initialize it:
private Collection<NetworkInterface> networkInterfaceCollection = new CollectionType<>();
As for persisting, your assumptions are correct but I think the execution is wrong. When you create your device, make it persistent with JPA right away (doing transaction management wherever needed).
Device device = new Device("laptop");
getEntityManager().persist(device);
Do the same for the NetworkInterface:
NetworkInterface net = new NetworkInterface("eth0");
getEntityManager().persist(net);
Now since both your entities are persisted, you can add one to the other.
device.getNetworkInterfaceCollection().add(net);
JPA should take care of the rest without you having to call any other persists.
This is a known behavior of collection data members.
The easiest solution is to modify your collection getter to lazily create the collection.
#XmlTransient
public Collection<NetworkInterface> getNetworkInterfaceCollection() {
if (networkInterfaceCollection == null) {
networkInterfaceCollection = new Some_Collection_Type<NetworkInterface>();
}
return networkInterfaceCollection;
}
Also, remember to refer to this data member only through the getter method.
This exception means you're trying to locate an entity (probably by em.getReference()) that hasn't been persisted yet.
You cannot you em.getReference() or em.find() on entities which still don't have a PK.
In order to enable save ability on a #OneToMany relation e.g.
#OneToMany(mappedBy="myTable", cascade=CascadeType.ALL)
private List<item> items;
Then you have to tell to your #ManyToOne relation that it is allowed to update myTable like this updatable = true
#ManyToOne #JoinColumn(name="fk_myTable", nullable = false, updatable = true, insertable = true)
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.