I want to assign a category to a recipe. If I assign a second category with the same name to the recipe it does another insert to the database that aborts (Abort due to constraint violation (UNIQUE constraint failed: category.name) - this is actually fine). I want to reuse this entry and attach it to the recipe. Is there a JPA way to do this "automatically" or do I have to handle this? Should I search for a category with the same name in the setCategory method and use this one if present? Is there a Pattern?
#Entity
public class Recipe {
private Integer id;
private Category category;
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "id")
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
#ManyToOne(cascade = CascadeType.PERSIST)
#JoinColumn(name = "category_id", referencedColumnName = "id")
public Category getCategory() {
return category;
}
public void setCategory(Category category) {
this.category = category;
}
}
#Entity
public class Category {
private Integer id;
private String name;
private List<Recipe> recipes;
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "id")
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
#Basic
#Column(name = "name")
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
#OneToMany(mappedBy = "category", cascade = {CascadeType.ALL})
public List<Recipe> getRecipes() {
return recipes;
}
public void setRecipes(List<Recipe> recipes) {
this.recipes = recipes;
}
}
Example:
Category category = new Category();
category.setName("Test Category");
cookbookDAO.add(cookbook);
Recipe recipe = new Recipe();
recipe.setTitle("Test Recipe");
recipe.setCategory( category );
recipeDAO.add(recipe);
Executing this twice results in the UNIQUE constraint failed: category.name. This is fine since I don't want multiple categories with the same name. The database enforced this but I'm looking for the soltuion to enforce this on the java language level too.
The DDL:
CREATE TABLE "recipe"
(
id INTEGER PRIMARY KEY AUTOINCREMENT,
category_id INTEGER,
FOREIGN KEY (category_id) REFERENCES category(id)
);
CREATE TABLE "category"
(
id INTEGER PRIMARY KEY AUTOINCREMENT,
`name` VARCHAR,
UNIQUE(`name`) ON CONFLICT ABORT
);
Hello the behavior you are describing is a result of the mapping
#ManyToOne(cascade = CascadeType.PERSIST)
#JoinColumn(name = "category_id", referencedColumnName = "id")
public Category getCategory() {
return category;
}
If we translate this mapping is simple language. You are saying:
Everytime I attempt to save a Recipe the corresponding Category should
be persisted as well.
The problem here comes from the fact that you already have an existing Category so the #Cascade.PERSIST here is not appropriate.
The semantics here is the opposite . A Recipie creates a Category only if a Category does not exist yet. This mean that the creation of the Category is more of an exception than a general rule.
This mean that you have two options here.
Option 1 is to remove Cascade.PERSIST.
Option 2 is to replace it with Cascade.MERGE.
Option 3 is to go the other way. Instead of annotating the #ManyToOne relationship in Recipe to Category with Cascade.PERSIST to annotate the #OneToMany relationship from the Category to Recipe.
The advantage of such approach is very simple. While you not always want to create a new category when adding a new recipe. It is 100% all the time you want to add a new Category you also want to add all the attached Recipies.
Also I will recommend you to favor Bidirectional relationships over unidirectional ones and read this article about merge and persist JPA EntityManager: Why use persist() over merge()?
The problem is you are creating a new category with the following statement:
Category category = new Category();
Because this instance of the Category entity is new and accordingly does not have a persistent identity the persistence provider will try to create a new record in the database. As the name field of the category table is unique you get constraint violation exception from the database.
The solution is first fetching the category and assign it to recipe. So what you have to do is the following:
String queryString = "SELECT c FROM Category c WHERE c.name = :catName";
TypedQuery<Category> query = em.createQuery(queryString, Category.class);
em.setParameter("catName", "Test Category");
Category category = query.getSingleResult();
This fetched instance is a managed entity and the persistence provider will not try to save. Then assign this instance to the recipe as you already did:
recipe.setCategory( category );
In this case the cascading will just ignore saving the category instance when recipe is saved. This is stated in the JPA 2.0 specification in section 3.2.2 Persisting an Entity Instance as follows:
If X is a preexisting managed entity, it is ignored by the persist operation.
Related
I have 2 DTOs "OrderItem" and "Ingredient", both classes has #ManyToMany annotation:
#Entity
#Table
#NoArgsConstructor
#Data
public class OrderItem {
private #Id #GeneratedValue #NotNull long id;
#ManyToOne(optional = false)
#JoinColumn(nullable = false)
#OnDelete(action = OnDeleteAction.CASCADE)
private Order order;
#ManyToOne(optional = false)
#JoinColumn(nullable = false)
#OnDelete(action = OnDeleteAction.CASCADE)
private Food food;
private int quantity;
#ManyToMany(cascade=CascadeType.ALL)
#JoinTable(
name = "order_item_ingredient",
joinColumns = #JoinColumn(name = "order_item_id"),
inverseJoinColumns = #JoinColumn(name = "ingredient_name")
)
private Set<Ingredient> ingredients = new HashSet<>();
}
#Entity
#Table
#Data
#NoArgsConstructor
public class Ingredient {
private #Id String ingredientName;
private float basePrice;
private boolean addable;
#ManyToMany(mappedBy = "ingredients",cascade=CascadeType.ALL)
private Set<Food> foods= new HashSet<>();
#ManyToMany(mappedBy = "ingredients",cascade=CascadeType.ALL)
private Set<OrderItem> orderItems= new HashSet<>();
public Ingredient(String ingredientName, float basePrice, boolean addable) {
this.ingredientName = ingredientName.toLowerCase();
this.basePrice = basePrice;
this.addable = addable;
}
}
And I'm looking to add a new OrderItem using a POST request using the following #PostMapping controller function:
#PostMapping("{id}/orderItem")
public ResponseEntity<OrderItem> createMenuItem(
#PathVariable(value = "id") Long orderId,
#RequestBody OrderItem orderItem) {
Order order = orderService.getOrder(orderId)
.orElseThrow(() -> new ResourceNotFoundException("order '" + orderId + "' is not found"));
orderItem.setOrder(order);
orderItemRepository.save(orderItem);
return new ResponseEntity<>(orderItem, HttpStatus.CREATED);
}
When I send a post request to localhost:8080/1/orderItem with the following body:
{
"order":"1",
"food":"burger",
"quantity":"1"
}
It works fine and a new order_item database record is created, but when I send the same request with the following body:
{
"order":"1",
"food":"burger",
"quantity":"1",
"ingredients": [{"ingredientName":"leaf"}]
}
It fails and gives the following SQL error:
java.sql.SQLIntegrityConstraintViolationException: Duplicate entry 'leaf' for key 'ingredient.PRIMARY'
I know that this record already exists, but how do I tell Spring Boot that I want it to look for an existing Ingredient instead of trying to create a new one?
I have an ugly solution in my mind, and that is to send the OrderItem object alongside a list of strings where each element represents a primary key for Ingredient class, then iterate through that list element by element calling the repository to get the Ingredient object then manually add it to OrderItem.ingredients, but I'm sure that is not the best solution out there.
Being defined on the OrderItem class, the relation ingredients is considered as a composition on the cascading strategy point of view. Therefore, the CascadeType.ALL implies the attempt to create the ingredient.
To avoid this, you can change the direction of this relation reverse the mappedBy information.
But then again, if you keep a CascadeType.ALL on the ingredient side, you will be in trouble if you create an ingredient with an existing orderItem. You can win on both sides an use CascadeType.ALL.
check JPA Hibernate many-to-many cascading
I have two entity class named Customer and Activity.
Customer has customerId,customerName,activities here activities holds set of Activity corresponds to each Customer mapped by #OneToMany(mappedBy = "customer") relationship.
The Customer class has been defined as follows (I have removed other fields and some getter and setter for clarity):
#javax.persistence.Entity
#Table(name = "CUSTOMER")
public class Customer extends Entity {
private String customerId;
private String customerName;
private Set<Activity> activities;
#NaturalId
#Column(name = "CUSTOMER_ID", nullable = false)
public String getCustomerId() {
return customerId;
}
#Column(name = "CUSTOMER_NAME", nullable = false)
public String getCustomerName() {
return customerName;
}
#OneToMany(mappedBy = "customer", fetch = FetchType.LAZY)
#Cascade(CascadeType.SAVE_UPDATE)
public Set<Activity> getActivities() {
return activities;
}
}
Activity has activityId,activityName,customer which is mapped by customerId with #ManyToOne relation.
Activity class is defined like:
#javax.persistence.Entity
#Table(name = "ACTIVITY")
public class Activity extends Entity {
private String activityId;
private String activityName;
private Customer customer;
#NaturalId
#Column(name = "ACTIVITY_ID", nullable = false)
public String getActivityId() {
return activityId;
}
#Column(name = "ACTIVITY_NAME", nullable = false)
public String getActivityName() {
return activityName;
}
#ManyToOne
#JoinColumn(name = "CUSTOMER_ID", nullable = false)
public Customer getCustomer() {
return customer;
}
}
Before saving a new Activity, I am adding this Activity to the activities set of Customer.
I want to know:
Where the activities of Customer are getting saved ?
How the update and delete operation of both of the entities will effect each other?
Is it the right way to create #OneToMany and #ManyToOne relationship?
Can I add column name on activities?
I am beginner in hibernate, any pointer would be very helpful to me.
Thanks in advance.
- Where the activities of Customer are getting saved ?
They are not getting saved as "activities of customer" anywhere in the DB.When the activities of a customer are needed, they are loaded together with the customer using a join query on the customer_id column, or they are loaded later (lazy-loading) using a separate query that filters the customer_id column.
- How the update and delete operation of both of the entities will effect each other?
A delete of an activity will not affect the customer, but next time its activities are loaded, the deleted activity will not be part of the collection anymore.
How the delete of a customer activities can be defined using the cascade attribute on the #ManyToOne, for example :
#OneToMany(mappedBy = "customer", cascade = CascadeType.REMOVE)
Basically, the desired behavior depends on if an activity can exist on its own without a customer : if yes, then the customer can be deleted, and the customer_id field can be set to null on the activity table. Otherwise the activities belonging to a customer will be 'cascade-deleted'. Or a customer just can0t be deleted before one has removed all its activities first.
- Is it the right way to create #OneToMany and #ManyToOne relationship?
Yes, it is correct.
- Can I add column name on activities?
No, it wouldn't make any sense. A column could only contain one ID, and activities is a collection.
I hope this helps.
I have two tables: Recipe table and Account table. The Recipe table stores a number of recipes. The account table stores a number of user account. A user can be associated with 0 or more recipe. When a user likes a recipe, this user is associated to the recipe. To record this association, I created a table called LikedRecipe table.
These are the columns of each table:
Recipe: id, name. Id is the primary key.
Account: email, password. Email is the primary key.
LikedRecipe: id, name, email. id is the primary key.
#Entity
#Table(name = "Recipe")
public class Recipe {
private Set<Account> account;
private Long id;
#ManyToMany(fetch = FetchType.LAZY)
#JoinTable(name = "LikedRecipe", joinColumns = #JoinColumn(name = "recipeId"), inverseJoinColumns = #JoinColumn(name = "email"))
public Set<Account> getAccount() {
return account;
}
public void setAccount(Set<Account> account) {
this.account = account;
}
#Id
#GeneratedValue(strategy = IDENTITY)
#Column(name = "id")
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
}
#Entity
#Table(name = "Account")
public class Account implements Serializable {
private Set<Recipe> likedRecipes = new HashSet<Recipe>();
private String email;
#ManyToMany(fetch = FetchType.LAZY, cascade = { CascadeType.ALL })
#JoinTable(name = "LikedRecipe", joinColumns = #JoinColumn(name = "email"), inverseJoinColumns = #JoinColumn(name = "recipeId"))
public Set<Recipe> getLikedRecipes() {
return likedRecipes;
}
public void setLikedRecipes(Set<Recipe> likedRecipes) {
this.likedRecipes = likedRecipes;
}
#Column(name = "email")
#Id
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
}
I wrote a method to remove the association between an account and a recipe:
public void unlikeARecipe(String email, Long recipeId){
Query query = entityManager
.createQuery("delete from LikedRecipe where recipeId = :recipeId and email = :email");
query.setParameter("recipeId", recipeId);
query.setParameter("email", email);
query.executeUpdate();
}
This method did not delete records from LikedRecipe table, until I added this line of code at the end of the method:
entityManager.clear();
According to JPA API documentation the clear method clears the persistence context, causing all managed entities to become detached.
My question is what does detach means ? And, how does detaching objects made the above method deletes records from LikedRecipe table? Am I using the clear method in the right manner ?
Thank you.
Detached entity is an entity not currently managed by a persistence context but whose id is present in database.
I think you don't get the LikedRecipe entity deleted because you still have it referenced from other persistent entities (Account and Recipe). Indeed it works when you clear the persistence context, detaching all entities that are "keeping alive" the LikedRecipe you wanted to delete.
If you want to keep the many-to-many relationships, you have to clear them as well (i.e. removing the object from Account's and Recipe's collections) when you're about to deleting the LikedRecipe entity.
Shouldn´t be better to make design something like:
- Recipe(id,name)
- User(email, ...)
- LikedRecipe(userEmail, recipeId)?
And don´t make relationship in recipe for users, it´s (in my humble opinion) useless relationship.
To this case it´s enough to make OneToMany relationship from User (/Account) to Recipe and none relation in Recipe for Users.
I have a following SQL schema layout:
-- postgresql82+ syntax
create table AudioTracks (
id serial primary key
, name text
, size integer
, filePath text
, additionDate timestamp default now()
);
create table Genres (
id serial primary key
, name text unique -- here is the unique constraint which troubles me
, description text
, additionDate timestamp default now()
);
create table AudioTrackGenre (
genreId integer references Genres (id) unique
, audioTrackId integer references AudioTracks (id) unique
, additionDate timestamp default now()
);
And two corresponding mappings to tables:
#Entity(name = "AudioTracks")
public class AudioTrack implements Serializable {
#Id
#Column
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
#Column
private String name;
#Column
private Integer size;
#Column
private String filePath;
#Column
#Temporal(TemporalType.TIMESTAMP)
private Date additionDate;
#ManyToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL )
#JoinTable(name = "AudioTrackGenre",
joinColumns = { #JoinColumn(name = "audioTrackId") },
inverseJoinColumns = { #JoinColumn(name = "genreId") }
)
#OrderBy("name")
private Set<Genre> genres = new HashSet<Genre>();
// setter/getter methods //
....
}
and
#Entity(name = "Genres")
public class Genre implements Serializable {
#Id
#Column
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
#Column
private String name;
#Column
private String description;
#Column
#Temporal(TemporalType.TIMESTAMP)
private Date additionDate;
#ManyToMany(mappedBy = "genres")
private Set<AudioTrack> audioTracks = new HashSet<AudioTrack>();
public Genre() { }
public Genre(String name) { this.name = name; }
// setter/getter methods //
....
}
But whenever i am trying to save AudioTrack, populated with Genres which are already exists in Genres table, like here:
Set<Genre> genres = new HashSet<Genre>();
genres.add(new Genre("ambient"));
AudioTrack track = new AudioTrack();
track.setGenres(genres);
audioTrackService.addAudioTrack(track);
(the audioTrackService.addAudioTrack(track) thing does sessionFactory.getCurrentSession().save(track) at lowest DAO level)
i am getting:
ERROR: ERROR: duplicate key value violates unique constraint "genres_name_key"
Detail: Key (name)=(ambient) already exists.
How do i tell Hibernate not to try to re-insert already existing genres to Genres table on cascade inserts?
If the genre already exists, you must provide its id.
Look at this line: new Genre("ambient").
How could hibernate possibly guess what is the existing id of the Genre ambient?
Hibernate tries to insert the corresponding genre, because you haven't provided its id.
When you insert the audio track, hibernate must inserts records in the AudioTrackGenre table. Hibernate must know the ids of the genres. Otherwise hibernate assumes they are new genres.
Edit:
It seems you are adding genres on demand(like StackOverflow tags).
You can do the following in your code:
for (String genreName : submittedTextGenres) {
Genre genre = genreDAO.findByName(genre);
if (genre == null) { //a new genre
genre = new Genre(genreName);
}
audioTrack.addGenre(genre);
}
If you are afraid of a concurrent user adding the same genres: (suggestion by JB Nizet)
for (String genreName : submittedTextGenres) {
Genre genre = genreDAO.findByName(genre);
if (genre == null) { //a new genre
try {
genre = genreDAO.insertGenre(genre); //a transaction
} catch (GenreExistsException) {
genre = genreDAO.findByName(genre); //a separate transaction
}
}
audioTrack.addGenre(genre);
}
I'm trying to set up the following tables using JPA/Hibernate:
User:
userid - PK
name
Validation:
userid - PK, FK(user)
code
There may be many users and every user may have max one validation code or none.
Here's my classes:
public class User
{
#Id
#Column(name = "userid")
#GeneratedValue(strategy = GenerationType.IDENTITY)
protected Long userId;
#Column(name = "name", length = 50, unique = true, nullable = false)
protected String name;
...
}
public class Validation
{
#Id
#Column(name = "userid")
protected Long userId;
#OneToOne(cascade = CascadeType.ALL)
#PrimaryKeyJoinColumn(name = "userid", referencedColumnName = "userid")
protected User user;
#Column(name = "code", length = 10, unique = true, nullable = false)
protected String code;
...
public void setUser(User user)
{
this.user = user;
this.userId = user.getUserId();
}
...
}
I create a user and then try to add a validation code using the following code:
public void addValidationCode(Long userId)
{
EntityManager em = createEntityManager();
EntityTransaction tx = em.getTransaction();
try
{
tx.begin();
// Fetch the user
User user = retrieveUserByID(userId);
Validation validation = new Validation();
validation.setUser(user);
em.persist(validation);
tx.commit();
}
...
}
When I try to run it I get a org.hibernate.PersistentObjectException: detached entity passed to persist: User
I have also tried to use the following code in my Validation class:
public void setUserId(Long userId)
{
this.userId = userId;
}
and when I create a validation code I simply do:
Validation validation = new Validation();
validation.setUserId(userId);
em.persist(validation);
tx.commit();
But then since User is null I get org.hibernate.PropertyValueException: not-null property references a null or transient value: User.code
Would appreciate any help regarding how to best solve this issue!
I have been able to solve this problem of "OneToOne between two tables with shared primary key" in pure JPA 2.0 way(Thanks to many existing threads on SOF). In fact there are two ways in JPA to handle this. I have used eclipselink as JPA provider and MySql as database. To highlight once again no proprietary eclipselink classes have been used here.
First approach is to use AUTO generation type strategy on the Parent Entity's Identifier field.
Parent Entity must contain the Child Entity Type member in OneToOne relationship(cascade type PERSIST and mappedBy = Parent Entity Type member of Child Entity)
#Entity
#Table(name = "USER_LOGIN")
public class UserLogin implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#Column(name="USER_ID")
private Integer userId;
#OneToOne(cascade = CascadeType.PERSIST, mappedBy = "userLogin")
private UserDetail userDetail;
// getters & setters
}
Child Entity must not contain an identifier field. It must contain a member of Parent Entity Type with Id, OneToOne and JoinColumn annotations. JoinColumn must specify the ID field name of the DB table.
#Entity
#Table(name = "USER_DETAIL")
public class UserDetail implements Serializable {
#Id
#OneToOne
#JoinColumn(name="USER_ID")
private UserLogin userLogin;
// getters & setters
}
Above approach internally uses a default DB table named SEQUENCE for assigning the values to the identifier field. If not already present, This table needs to be created as below.
DROP TABLE TEST.SEQUENCE ;
CREATE TABLE TEST.SEQUENCE (SEQ_NAME VARCHAR(50), SEQ_COUNT DECIMAL(15));
INSERT INTO TEST.SEQUENCE(SEQ_NAME, SEQ_COUNT) values ('SEQ_GEN', 0);
Second approach is to use customized TABLE generation type strategy and TableGenerator annotation on the Parent Entity's Identifier field.
Except above change in identifier field everything else remains unchanged in Parent Entity.
#Entity
#Table(name = "USER_LOGIN")
public class UserLogin implements Serializable {
#Id
#TableGenerator(name="tablegenerator", table = "APP_SEQ_STORE", pkColumnName = "APP_SEQ_NAME", pkColumnValue = "USER_LOGIN.USER_ID", valueColumnName = "APP_SEQ_VALUE", initialValue = 1, allocationSize = 1 )
#GeneratedValue(strategy = GenerationType.TABLE, generator = "tablegenerator")
#Column(name="USER_ID")
private Integer userId;
#OneToOne(cascade = CascadeType.PERSIST, mappedBy = "userLogin")
private UserDetail userDetail;
// getters & setters
}
There is no change in Child Entity. It remains same as in the first approach.
This table generator approach internally uses a DB table APP_SEQ_STORE for assigning the values to the identifier field. This table needs to be created as below.
DROP TABLE TEST.APP_SEQ_STORE;
CREATE TABLE TEST.APP_SEQ_STORE
(
APP_SEQ_NAME VARCHAR(255) NOT NULL,
APP_SEQ_VALUE BIGINT NOT NULL,
PRIMARY KEY(APP_SEQ_NAME)
);
INSERT INTO TEST.APP_SEQ_STORE VALUES ('USER_LOGIN.USER_ID', 0);
If you use Hibernate you can also use
public class Validation {
private Long validationId;
private User user;
#Id
#GeneratedValue(generator="SharedPrimaryKeyGenerator")
#GenericGenerator(name="SharedPrimaryKeyGenerator",strategy="foreign",parameters = #Parameter(name="property", value="user"))
#Column(name = "VALIDATION_ID", unique = true, nullable = false)
public Long getValidationId(){
return validationId;
}
#OneToOne
#PrimaryKeyJoinColumn
public User getUser() {
return user;
}
}
Hibernate will make sure that the ID of Validation will be the same as the ID of the User entity set.
Are you using JPA or JPA 2.0 ?
If Validation PK is a FK to User, then you do not need the Long userId attribute in validation class, but instead do the #Id annotation alone. It would be:
Public class Validation
{
#Id
#OneToOne(cascade = CascadeType.ALL)
#PrimaryKeyJoinColumn(name = "userid", referencedColumnName = "userid")
protected User user;
#Column(name = "code", length = 10, unique = true, nullable = false)
protected String code;
...
public void setUser(User user)
{
this.user = user;
this.userId = user.getUserId();
}
...
}
Try with it and tell us your results.
You need to set both userId and user.
If you set just the user, then the id for Validation is 0 and is deemed detached. If you set just the userId, then you need to make the user property nullable, which doesn't make sense here.
To be safe, you can probably set them both in one method call:
#Transient
public void setUserAndId(User user){
this.userId = user.getId();
this.user = user;
}
I marked the method #Transient so that Hibernate will ignore it. Also, so you can still have setUser and setUserId work as expected with out any "side effects."