Hibernate: How to update parent_id field without retrieving parent from db? - java

In a Spring Boot app, I have the following entities that have one-to-many relationship (Category is the parent of Recipe):
#Entity
public class Recipe {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#Column(nullable = false, length = 50)
private String title;
#ManyToOne(optional = true, fetch = FetchType.LAZY)
#JoinColumn(name = "category_id", referencedColumnName = "id")
private Category category;
}
#Entity
public class Category {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#Column(unique = true, nullable = false, length = 50)
private String name;
#OneToMany(mappedBy = "category", cascade = CascadeType.ALL)
private Set<Recipe> recipes = new HashSet<>();
public void addRecipe(Recipe recipe) {
recipes.add(recipe);
recipe.setCategory(this);
}
public void removeRecipe(Recipe recipe) {
recipes.remove(recipe);
recipe.setCategory(null);
}
}
When I create a Recipe, I send categoryId that is selected from Dropdown list and create Recipe by retrieving and adding category to the recipe as shown below:
#Transactional
public void update(RecipeRequest request) {
final Category category = categoryRepository.findById(request.getCategoryId())
.orElseThrow(() -> new NoSuchElementFoundException(NOT_FOUND_CATEGORY));
/* instead of retrieving category, I want to set the categoryId field of Recipe,
but there is not such kind of setter */
recipe.setCategoryId(request.getCategoryId());
recipe.setTitle(capitalizeFully(request.getTitle()));
recipe.setCategory(category);
recipeRepository.save(recipe);
}
Instead of retrieving category, I want to set the categoryId field of Recipe, but there is not such kind of setter:
recipe.setCategoryId(request.getCategoryId());
So, what is the most proper way for just setting the categoryId of the recipe and then saving it without requiring the category from db? Do I need a setter for categoryId field to the Recipe (I thought it, but does not seem elegant way)?

I would just add a categoryId field along with the corresponding getter and settter methods to the Recipe class.
#Entity
public class Recipe {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#Column(nullable = false, length = 50)
private String title;
#ManyToOne(optional = true, fetch = FetchType.LAZY)
#JoinColumn(name = "category_id", referencedColumnName = "id")
private Category category;
#Column(name = "category_id", nullable = false)
private Integer categoryId;
// getters/setters
}
Having a categoryId field means that when we don't have to create an instance of Category when adding new Recipes. Sure, Recipe.category will be null but that's ok if we're just adding new Recipes. This approach could also prove beneficial if we later decide that we need to add many Recipes simultaneously.

If your repository implements org.springframework.data.jpa.repository.JpaRepository you may take advantage of using JpaRepository#getReferenceById method, in that case Hibernate instead of querying DB for data will return proxy object. However, such implementation may cause issues in some cases, for example:
// this call typically returns entity
// or null if entity wasn't found
repository.findById(id);
but:
// this call returns proxy object
repository.getReferenceById(id);
// now instead of returning entity
// repository either returns initialized proxy
// object or throws EntityNotFoundException
// if entity wasn't found
repository.findById(id);

Related

Hibernate #OneToMany reference child not updated / lost?

Say I have three entities.
#Entity
public class Process {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#Column(unique = true)
private String name;
#ManyToAny(
metaColumn = #Column(name = "node_type"),
fetch = FetchType.LAZY
)
#AnyMetaDef(
idType = "long", metaType = "string",
metaValues = {
#MetaValue(targetEntity = Milestone.class, value = MILESTONE_DISC),
#MetaValue(targetEntity = Phase.class, value = PHASE_DISC)
}
)
#Cascade({org.hibernate.annotations.CascadeType.ALL})
#JoinTable(
name = "process_nodes",
joinColumns = #JoinColumn(name = "process_id", nullable = false),
inverseJoinColumns = #JoinColumn(name = "node_id", nullable = false)
)
private Collection<ProcessNode> nodes = new ArrayList<>();
...
}
#Entity
#ToString
#DiscriminatorValue(MILESTONE_DISC)
public class Milestone implements ProcessNode {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
#OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
private Collection<ResultDefinition> results;
#ManyToOne()
private Process process;
...
}
#Entity
#ToString
public class ResultDefinition {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String externalId;
private String name;
private ResultType resultType;
}
From my client I want to add an Object of type ResultDefinition to a Milestone in a Process like this:
#Transactional
#PostMapping("/{milestone_id}/results")
public ResultDefinitionDto createResult(#PathVariable("milestone_id") Long milestoneId, #RequestBody ResultDefinitionDto dto) {
Process foundProcess = getProcess(milestoneId);
checkFoundProcess(milestoneId, foundProcess);
Milestone milestone = getMilestone(foundProcess, milestoneId);
ResultDefinition resultDefinition = resultDefinitionMapper.fromDTO(dto);
milestone.addResult(resultDefinition);
processService.save(foundProcess);
//TODO: Find out why this is necessary (???)
ResultDefinition savedResult = milestone.getResult(resultDefinition.getName());
return resultDefinitionMapper.fromEntity(savedResult);
}
In my method createResult I add resultDefinition to the milestone results collection.
When I save the parent foundProcess, I see that foundprocess->milestone->resultDefinition get's persisted and gets an ID. When I call resultDefinition.getId() it returns null. Also the ResultDefinition Object in the foundProcess is another reference and not the same that I added to milestone.results.
Why do I get the correct instance when calling milestone.getResult()?
Edit: my implementation of processService / repository
#Override
public Process save(Process entity) {
return processRepository.saveAndFlush(entity);
}
public interface ProcessRepository extends JpaRepository<Process, Long>, JpaSpecificationExecutor<Process> {
...
}
The ResultDefinition gets replaced during the save process. The transient entity gets inserted into the database and will be replaced through a managed entity with an id. Your reference to the ResultDefinition in the createResult method still points to the transient one. That´s why you have to work with the returned entities from a save call.
In your case you are saving the parent process. So you have to access the saved ResultDefinition through the process or milestone entity.
You can try add flush method after save.

JPA: Reference column in the child entity is null when using unidirectional #OneToMany

I have two entity classes.
Order.java
#Entity
#Table(name = "order_table")
public class Order implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#Column(name = "id")
private Long id;
#Column(name = "name")
private String name;
#OneToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL, orphanRemoval = true)
#JoinColumn(name = "order_id", referencedColumnName = "id", nullable = false, insertable=false, updatable=false)
private Set<Item> items;
// getters & setters & toString
Item.java
#Entity
#Table(name = "item")
public class Item implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#Column(name = "id")
private Long id;
#Column(name = "name")
private String name;
#Column(name = "order_id", nullable = false)
private Long orderId;
// getters & setters && toString
I created a test class like this:
#Test
public void createOrderWithItems() {
Item item = new Item();
item.setName("Iron Man");
Order order = new Order();
order.setName("Toy");
order.getItems().add(item);
Order created = service.createOrder(order);
Order orderById = service.getOrderById(order.getId());
System.out.println("Created Order: " + orderById);
Item itemById = service.getItemById(item.getId());
System.out.println("Created item: " + itemById);
Assert.notNull(created.getId(), "Order ID is Null");
}
Test is green but if you check output, you'll see that orderId field in the Item class is null.
Created Order: Order{id=1, name='Toy', items=[Item{id=2, name='Iron Man', orderId=null}]}
Created item: Item{id=2, name='Iron Man', orderId=null}
Does JPA not update this column in the db automatically? Is this column is redundant? If so, how can I retrieve this information from test code?
You need to set orderId explicitly.
item.setOrderId(order.getId());
order.getItems().add(item);
You can create a method addItem(Item item) in your Order class and hide this logic within it.
Cascading will create an entry in db but it won't initialize field. JPA annotations just indicate to JPA provider how to perform mapping between entity and table.
Moreover, check your annotations. #JoinColumn should be used in the entity which owns the relationship (the corresponding table has column as a foreign key). Check the top answer for this question for detailed explanations: What's the difference between #JoinColumn and mappedBy when using a JPA #OneToMany association

JPA: How to handle versioned entities?

I have a versioning on an entity as part of its primary key. The versioning is done via a timestamp of the last modification:
#Entity
#Table(name = "USERS")
#IdClass(CompositeKey.class)
public class User {
#Column(nullable = false)
private String name;
#Id
#Column(name = "ID", nullable = false)
private UUID id;
#Id
#Column(name = "LAST_MODIFIED", nullable = false)
private LocalDateTime lastModified;
// Constructors, Getters, Setters, ...
}
/**
* This class is needed for using the composite key.
*/
public class CompositeKey {
private UUID id;
private LocalDateTime lastModified;
}
The UUID is translated automatically into a String for the database and back for the model. The same goes for the LocalDateTime. It gets automatically translated to a Timestamp and back.
A key requirement of my application is: The data may never update or be deleted, therefore any update will result in a new entry with a younger lastModified. This requirement is satisfied with the above code and works fine until this point.
Now comes the problematic part: I want another object to reference on a User. Due to versioning, that would include the lastModified field, because it is part of the primary key. This yields a problem, because the reference might obsolete pretty fast.
A way to go might be depending on the id of the User. But if I try this, JPA tells me, that I like to access a field, which is not an Entity:
#Entity
#Table(name = "USER_DETAILS")
public class UserDetail {
#Id
#Column(nullable = false)
private UUID id;
#OneToOne(optional = false)
#JoinColumn(name = "USER_ID", referencedColumnName = "ID")
private UUID userId;
#Column(nullable = false)
private boolean married;
// Constructors, Getter, Setter, ...
}
What would be the proper way of solving my dilemma?
Edit
I got a suggestion by JimmyB which I tried and failed too. I added the failing code here:
#Entity
#Table(name = "USER_DETAILS")
public class UserDetail {
#Id
#Column(nullable = false)
private UUID id;
#OneToMany
#JoinColumn(name = "USER_ID", referencedColumnName = "ID")
private List<User> users;
#Column(nullable = false)
private boolean married;
public User getUser() {
return users.stream().reduce((a, b) -> {
if (a.getLastModified().isAfter(b.getLastModified())) {
return a;
}
return b;
}).orElseThrow(() -> new IllegalStateException("User detail is detached from a User."));
}
// Constructors, Getter, Setter, ...
}
What you seem to require seems to be on the lines of a history table, to keep track of the changes. See https://wiki.eclipse.org/EclipseLink/Examples/JPA/History on how EclipseLink can handle this for you while using normal/traditional JPA mappings and usage.
What you have here is a logical 1:1 relationship which, due to versioning, becomes a technical 1:n relationship.
You have basically three options:
Clean JPA way: Declare an 'inverse' #ManyToOne relationship from user to the "other object" and make sure you always handle it whenever a new User record is created.
'Hack-ish' way: Declare a #OneToMany relationship in the "other object" and force it to use a specific set of columns for the join using #JoinColumn. The problem with this is that JPA always expects unique reference over the join columns so that reading the UserDetail plus referenced User records should work, whereas writing UserDetail should not cascade onto User to avoid unwanted/undocumented effects.
Just store the user's UUID in the "other object" and resolve the reference yourself whenever you need it.
The added code in your question is wrong:
#JoinColumn(name = "USER_ID", referencedColumnName = "ID")
private UUID userId;
More correct, albeit not with the result you want, would be
#JoinColumn(name = "USER_ID", referencedColumnName = "ID")
private User user;
This won't work though, because, as I said above, you may have more than one user record per UserDetail, so you'd need a #OneToMany relationship here, represented by a Collection<User>.
Another 'clean' solution is to introduce an artificial entity with a 1:1 cardinality w.r.t. to the logical User to which you can refer, like
#Entity
public class UserId {
#Id
private UUID id;
#OneToMany(mappedBy="userId")
private List<User> users;
#OneToOne(mappedBy="userId")
private UserDetail detail;
}
#Entity
public class User {
#Id
private Long _id;
#ManyToOne
private UserId userId;
}
#Entity
public class UserDetail {
#OneToOne
private UserId userId;
}
This way, you can somewhat easily navigate from users to details and back.
I came to a solution, that is not really satisfying, but works. I created a UUID field userId, which is not bound to an Entity and made sure, it is set only in the constructor.
#Entity
#Table(name = "USER_DETAILS")
public class UserDetail {
#Id
#Column(nullable = false)
private UUID id;
#Column(nullable = false)
// no setter for this field
private UUID userId;
#Column(nullable = false)
private boolean married;
public UserDetail(User user, boolean isMarried) {
this.id = UUID.randomUUID();
this.userId = user.getId();
this.married = isMarried;
}
// Constructors, Getters, Setters, ...
}
I dislike the fact, that I cannot rely on the database, to synchronize the userId, but as long as I stick to the no setter policy, it should work pretty well.

How to insert in to multiple tables via Hibernate

I have two tables I need to insert in to in Hibernate - I have a User and every user belongs is a Member. Therfore when creating a new user I need a new entry in the Member table. I have attempted this via creating a Member object which maps to my member table and then having that as a field in my User object which maps to the user table
#Entity
#Table(name = "USER")
public class User
{
#Id
#GeneratedValue
#Column(name = "id")
private int id;
#Column(name = "username")
private String username;
#Column(name = "password")
private String password;
#Column(name = "fullName")
private String fullName;
//other fields ommited
#OneToOne
#JoinColumn(name = "id")
private Member member;
My member pojo looks as follows
#Entity
#Table(name = "MEMBER")
public class Member
{
#Id
#GeneratedValue
#Column(name = "id")
private int id;
#Column(name = "sgpid")
private int sgpid;
#Column(name = "username")
private String username;
Trying to save the object i do as follows;
#Override
public boolean addUser(User user)
{
if (user == null)
{
throw new IllegalArgumentException("Unable to add null user");
}
Session session = HibernateUtil.getSessionFactory().openSession();
session.beginTransaction();
session.save(user);
session.getTransaction().commit();
return true;
}
This gives me the row saved in my user table but the entry is not inserted in to the member table. I think my linking annotations are probably incorrect but I am not too sure - please could someone provide some assistance.
Thanks
Try to set the cascade value of the #OneToOne annotation:
#OneToOne(cascade = CascadeType.PERSIST)
#JoinColumn(name = "id")
private Member member;
First thing in your user class you should change the joinColumn to member_id.
As mentioned in another answer to persist a related entity you need to set the cascade to persist, i would recommend using cascade All which will involve the related entity in all operations check the doc
https://docs.oracle.com/cd/E19798-01/821-1841/bnbqm/index.html
#OneToOne(cascade = CascadeType.ALL)
#JoinColumn(name = "member_id")
private Member member;

JPA merge results in duplicate entry of foreign entity

QUESTIONS:
Does anyone know how to merge without having EntityManager trying to re-insert the foreign entity?
SCENARIO:
Just to set up a scenario that closely matches my case: I have two entities
#Entity
#Table(name = "login", catalog = "friends", uniqueConstraints =
#UniqueConstraint(columnNames = "username"))
public class Login implements java.io.Serializable{
private static final long serialVersionUID = 1L;
#Id
#GeneratedValue(strategy = IDENTITY)
#Column(name = "id", unique = true, nullable = false)
private Integer id;
#Column(name = "username", unique = true, nullable = false, length = 50)
private String username;
#Column(name = "password", nullable = false, length = 250)
private String password;
}
#Entity
#Table(name = "friendshiptype", catalog = "friends")
public class FriendshipType implements java.io.Serializable{
private static final long serialVersionUID = 1L;
#Id
#GeneratedValue(strategy = IDENTITY)
#Column(name = "id", unique = true, nullable = false)
private Integer id;
#OneToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "username")
private Login login;
#Column(name = "type", unique = true, length = 32)
private String type;
...//other fields go here
}
Both the Login entity and the FriendshipType entity are persisted to the database separately. Then, later, I need to merge a Login row with a FriendshipType row. When I call entityManager.merge(friendship), it tries to insert a new Login which of course results in the following error
Internal Exception: com.mysql.jdbc.exceptions.jdbc4.MySQLIntegrityConstraintViolationException: Duplicate entry 'myUserName1350319637687' for key 'username'
Error Code: 1062
Call: INSERT INTO friends.login (password, username) VALUES (?, ?)
My question, again, is how do I merge two objects without having enityManager trying to reinsert the foreign object?
Here is how I solve the problem. I finally figure the reason the merge is not resolving is because the login.id is auto generated by JPA. So since I really don't need an auto-generated id field, I remove it from the schema and use username as the #id field:
#Entity
#Table(name = "login", catalog = "friends", uniqueConstraints =
#UniqueConstraint(columnNames = "username"))
public class Login implements java.io.Serializable{
private static final long serialVersionUID = 1L;
#Id
#Column(name = "username", unique = true, nullable = false, length = 50)
private String username;
#Column(name = "password", nullable = false, length = 250)
private String password;
}
Another solution that occurred to me, which I didn't implement but may help someone else, should they need to have an auto-generated id field.
Instead of creating an instance of Login for the merger, get the instance from the database. What I mean is, instead of
Login login = new Login(); login.setUsername(username); login.setPassword(password);
Do rather
Login login = loginDao.getByUsername(username);
That way, a new id field is not generated making the entity seem different.
Thanks and up-votes to everyone for helping, especially to #mijer for being so patient.
You can make your #JoinColumn non updatable:
#JoinColumn(name = "login_id", updatable = false) // or
#JoinColumn(name = "username", referencedColumnName = "username", updatable= false)
Or try to refresh / fetch your Login entity again before merging the FriendshipType:
// either this
entityManager.refresh(friendship.getLogin());
// or this
final Login login = entityManager
.getReference(Login.class, friendship.getLogin().getId());
friendship.setLogin(login);
// and then
entityManager.merge(friendship);
But, as other suggested I belive that FriendshipType would be better represented by a #ManyToOne relationship or maybe by a Embeddable or ElementCollection
Update
Yet another option is to change the owning side:
public class Login implements java.io.Serializable {
#OneToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "friendshiptype_id")
private FriendshipType friendshipType;
// Other stuff
}
public class FriendshipType implements java.io.Serializable {
#OneToOne(fetch=FetchType.LAZY, mappedBy="friendshipType")
private Login login;
// Other stuff
}
This will affect your data model (login table will have a friendshiptype_id column instead of the other way around), but will prevent the errors that you are getting, since relationships are always maintained by the owning side.
Have you tried cascade=MERGE? I.e.
#OneToOne(fetch = FetchType.LAZY, cascade=CascadeType.MERGE)
#JoinColumn(name = "username")
private Login login;
UPDATE
Another possible option is to use #ManyToOne (it's save as the association is unique)
#ManyToOne(fetch = FetchType.LAZY, cascade=CascadeType.MERGE)
#JoinColumn(name = "username")
private Login login;
You can do it with your original #Id setup. i.e.
#Id
#GeneratedValue(strategy = IDENTITY)
#Column(name = "id", unique = true, nullable = false)
private Integer id;
You can, but you don't need to change to:
#Id
#Column(name = "username", unique = true, nullable = false, length = 50)
private String username;
The trick is you must start by loading from the DB, via em.find(...) or em.createQuery(...). Then the id is guaranteed to be populated with the right value from the DB.
Then you can detach the entity by ending a transaction (for a transaction-scoped entity manager in a session bean), or by calling em.detach(ent) or em.clear(), or by serialising the entity and passing it over the network.
Then you can update the entity, all the while, keeping the original id value.
Then you can call em.merge(ent) and you will still have the correct id. However, I believe the entity must already pre-exist in the persistent context of the entity manager at this instant, otherwise it will think that you have a new entity (with manually populated id), and try to INSERT on transaction flush/commit.
So the second trick is to ensure the entity is loaded at the point of the merge (via em.find(...) or em.query(...) again, if you have a new persistent context and not the original).
:-)

Categories