I have a RECIPE table that has OneToMany relationship with the INGREDIENT table because a single recipe can have many ingredients. The issue is that if a user deletes an ingredient (which sets all fields (ingredient_id and ingredient) to NULL by frontend), then the row containing relationship of both the tables RECIPE_INGREDIENT is deleted but the row in the Ingredient table still exists. Can't we tell Hibernate to delete that rows also?
Oracle table
create table recipe(id number primary key,
name varchar2(25) unique);
create table ingredient(ingredient_id number(4) primary key,
ingredient varchar2(40));
create table recipe_ingredient(recipe_id number(4),
ingredient_id number(4),
constraint recipe_fk foreign key(recipe_id)
references recipe(recipe_id),
constraint ingredient_fk foreign
key(ingredient_id) references
ingredient(ingredient_id));
Ingredient and Recipe POJO
#Entity
#Table(name = "ingredient", uniqueConstraints={
#UniqueConstraint(columnNames="INGREDIENT_ID")
})
public class Ingredient implements Serializable {
#Id
#Column(name = "INGREDIENT_ID", unique=true, nullable=false)
#SequenceGenerator(name="seq_ingredient", sequenceName="seq_ingredient")
#GeneratedValue(strategy=GenerationType.AUTO, generator="seq_ingredient")
private Integer ingredientId;
#Column(name = "INGREDIENT")
private String ingredient;
/*#ManyToOne(fetch=FetchType.EAGER)
#JoinColumn(name="RECIPE_ID")
private Recipe recipe;*/
//getter and setters
#Entity
#Table(name = "recipe")
public class Recipe implements Serializable {
#Id
#Column(name = "id")
private Integer id;
#Column(name = "name")
private String name;
#OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER, orphanRemoval = true)
#JoinTable(name = "recipe_ingredient", joinColumns = { #JoinColumn(name = "recipe_id") }, inverseJoinColumns = { #JoinColumn(name = "ingredient_id") })
private List<Ingredient> ingredients;
//getters and setter
}
DAO Code
public class RecipeDaoImpl implements RecipeDao {
public void addRecipe(Recipe recipe) {
getSession().saveOrUpdate(recipe);
}
}
Log that shows that the row in INGREDIENT table still exists whereas Hibernate is just deleting row from 'RECIPE_INGREDIENT' table.
Please see following that ingredient_id with null is deleted. In both cases, it is updating ingredient.recipe_id as NULL.
Received following from frontend:
RecipeController - Recipe[recipeId=126,name=Sandwich,ingredients=[Ingredient[ingredientId=270,ingredient=Salt],[ingredientId=<null>,quantity=<null>]]]
Hibernate: update RECIPE set NAME=? where RECIPE_ID=?
Hibernate: update ingredient set INGREDIENT=? where INGREDIENT_ID=?
Hibernate: delete from recipe_ingredient where recipe_id=?
Hibernate: insert into recipe_ingredient (recipe_id, ingredient_id) values (?, ?)
So the database table has,
INDREDIENT
INGREDIENT_ID INGREDIENT
271 Salt
272 Sugar
RECIPE_INDGREDIENT
RECIPE_ID INDREDIENT_ID
126 271
Have you implemented the equals() and hashcode() methods correctly in the Receipe and Indgredient classes? If not then that could be the cause why the rows in indgredient table are not deleted. Read this article for more details.
Related
My model (exemplified) is the following:
CREATE TABLE person (
id INT PRIMARY KEY,
name TEXT
...
);
CREATE TABLE team (
id INT PRIMARY KEY,
name TEXT
....
);
CREATE TABLE team_reference_persons (
team_id INT NOT NULL,
person_id INT NOT NULL,
uses_telephone BOOLEAN,
PRIMARY KEY (team_id, person_id),
FOREIGN KEY (team_id) REFERENCES team(id),
FOREIGN KEY (person_id) REFERENCES person(id)
);
And my JPA defintion:
#Entity
#Table(name = "team")
public class Team {
#Id
private Integer id;
#OneToMany
#JoinTable(name = "team_reference_persons", joinColumns = #JoinColumn(name = "team_id", referencedColumnName = "id"), inverseJoinColumns = #JoinColumn(name = "person_id", referencedColumnName = "id"))
private List<Person> teamReferencePersons;
...
}
#Entity
#Table(name = "person")
public class Person {
#Id
private UUID id;
private String name;
...
}
So far, so good, when all you need is the person list on the team. But now I need to add the team_reference_persons.uses_telephone property from the join table in my person domain, So I am looking for a way to keep the persons logic, while I create a new subclass.
private class TeamIndividual extends Person {
boolean uses_telephone;
}
Then changing List<Person> on Team entity by List<TeamIndividual>. Is that possible someway? JPA should be indicated in such smart way that it adds the join table property to the final target entity (on both read and save).
No need to extend TeamIndividual to Person.
Annotate TeamIndividual with #Table(name = "team_reference_persons")
Define fields(teamId,personId,uses_telephone) inside TeamIndividual
Annotate fields teamId and PersonId with #ManyToOne and #JoinColumn
Add List to Team without annotation
Try this,It will work..!!
I'm testing the underlying model of a HSQL database using Hibernate/Spring Boot and I've run into an issue I cannot find a solution to.
This is my simple test, I'm trying to create a shoebox entity and save it to the database with a User object set as the FK for Owner:
#TestConfiguration
static class ShoeboxServiceTestContextConfiguration {
#Bean
public ShoeboxService shoeboxService() {
return new ShoeboxService();
}
#Bean
public UserService userService() {
return new UserService();
}
}
#Autowired
UserService users;
#Autowired
ShoeboxService shoeboxes;
#Test
public void testSave()
{
System.out.println("save");
int userId = 1;
User user = new User(userId, "Foo", "hello#world.com");
user = users.save(user);
Shoebox sb = new Shoebox(user, "Name", "Context", "Comment", false);
UUID sbId = shoeboxes.save(sb).getId();
sb = shoeboxes.findOne(sbId);
assertNotNull(sb);
assertEquals(sb.getName(), "Name");
assertEquals(sb.getContext(), "Context");
assertEquals(sb.getComment(), "Comment");
assertEquals(sb.isShare(), false);
shoeboxes.deleteById(sbId);
users.deleteById(userId);
}
However when it gets it throws a
integrity constraint violation: unique constraint or index violation; SYS_PK_10126 table: USER
exception when it tries to save the Shoebox to the DB. It successfully persist the User, and it succeeds in persisting the Shoebox object when there is no Owner FK attached to it, but crashes when the FK is supplied.
Here is my User POJO:
#Entity
#Table(name="User")
#JsonIgnoreProperties({"hibernateLazyInitializer", "handler"})
public class User implements Serializable
{
#Id
#Column(name = "ID")
private long ID;
#Column(name = "Name")
private String name;
#Column(name = "Email")
private String email;
#OneToOne(fetch = FetchType.LAZY)
private Shoebox currentlySelectedBox;
#OneToMany(fetch = FetchType.LAZY)
#JsonManagedReference(value="shoebox_owner")
private List<Shoebox> shoeboxes;
// Contructors, Getters/Setters etc.
}
And my Shoebox POJO:
#Entity
#Table(name="Shoebox")
#JsonIgnoreProperties({"hibernateLazyInitializer", "handler"})
public class Shoebox implements Serializable
{
#Id
#Column(name="ID")
UUID ID;
#ManyToOne(cascade = CascadeType.ALL)
#JoinColumn(name="OwnerID")
#JsonBackReference(value="shoebox_owner")
User owner;
#Column(name="Name")
String name;
#Column(name="Context")
String context;
#Column(name="Comment")
String comment;
#Column(name="Shared")
boolean share;
#Column(name="CreationDate")
LocalDateTime creationDate;
// Contructors, Getters/Setters etc.
}
Here is the HSQL creation script for the DB:
CREATE MEMORY TABLE PUBLIC.SHOEBOX(ID BINARY(255) NOT NULL PRIMARY KEY,COMMENT VARCHAR(255),CONTEXT VARCHAR(255),CREATIONDATE TIMESTAMP,NAME VARCHAR(255),SHARED BOOLEAN,OWNERID BIGINT)
CREATE MEMORY TABLE PUBLIC.USER(ID BIGINT NOT NULL PRIMARY KEY,EMAIL VARCHAR(255),NAME VARCHAR(255),CURRENTLYSELECTEDBOX_ID BINARY(255),CONSTRAINT FK3T924ODM2BIK5543K0E3UEGP FOREIGN KEY(CURRENTLYSELECTEDBOX_ID) REFERENCES PUBLIC.SHOEBOX(ID))
CREATE MEMORY TABLE PUBLIC.USER_SHOEBOX(USER_ID BIGINT NOT NULL,SHOEBOXES_ID BINARY(255) NOT NULL,CONSTRAINT FK5W8WMFC5E9RMEK7VC4N76MQVQ FOREIGN KEY(SHOEBOXES_ID) REFERENCES PUBLIC.SHOEBOX(ID),CONSTRAINT FKIR9SOKRCOQ33LCQTNR0LDXO93 FOREIGN KEY(USER_ID) REFERENCES PUBLIC.SHOEBOXUSER(ID),CONSTRAINT UK_508XA86IDIHP04FQD3D6GF8D7 UNIQUE(SHOEBOXES_ID))
ALTER TABLE PUBLIC.SHOEBOX ADD CONSTRAINT FK3J9RQBYW5VQ0IRF3FWYPG7LAB FOREIGN KEY(OWNERID) REFERENCES PUBLIC.USER(ID)
Why is the exception being triggered? Is there something wrong with my annotations and PK/FK relationships between the objects?
Many Thanks.
The issue is
#ManyToOne(cascade = CascadeType.ALL)
With CascadeType.ALL, any operations will extend to the other entities. So in this case the save method is cascading on the shoebox's user attempting to save it again. Since you are using a static id of 1, it is causing a key constraint.
I am building a sample for ManyToMany relationship between: User(1) - ()AccessLevel() - (1)Role
I have implemented 3 classes in Java with hibernate implementation as follow:
Class User
#Entity
#Table(name="user")
public class User {
#Id
#GeneratedValue
#Column(name="USER_ID")
#ManyToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL)
#JoinTable(name = "access_level", joinColumns = {
#JoinColumn(name = "user_id", nullable = false, updatable = false) },
inverseJoinColumns = { #JoinColumn(name = "role_id", nullable = false, updatable = false) })
private Set<Role> roles = new HashSet<Role>(0);
Class Role
#Entity
#Table(name="role")
public class Role {
#Id
#GeneratedValue
#Column(name="role_id")
private Integer id;
#Column(name="role_name")
private String roleName;
Class AccessLevel
#Entity
#Table(name="access_level")
public class AccessLevel {
#Id
#GeneratedValue
private Integer id;
#Column(name="role_id")
private Integer roleId;
#Column(name="user_id")
private Integer userId;
Problem:
When I am persisting the User bean using merge method then an exception arise:
#Service
public class UserServiceImpl implements UserService {
#PersistenceContext
private EntityManager em;
#Override
#Transactional
public void save(User user) {
em.merge(user);
}
Exception
org.springframework.web.util.NestedServletException: Request process
ing failed; nested exception is org.springframework.dao.DataIntegrityViolationException: Could not execute JDBC batch update; SQL [insert into access_level (user_id, role_id) values (?, ?)]; constraint [null]; nested exception is org.hibernate.exception.ConstraintViolationException: Could not execute JDBC batch update
org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:894)
org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:789)
javax.servlet.http.HttpServlet.service(HttpServlet.java:641)
javax.servlet.http.HttpServlet.service(HttpServlet.java:722)
As you can see hibernate is trying to run this query:
insert into access_level (user_id, role_id) values (?, ?)
From my point of view it seems like hibernate is not generating the primary key for AccessLevel even though I have added the #GeneratedValue to the id attribute.
Note:
I am working on production environment with Postgresql and evelopment environment with HSQL database that creates all schemas from the begining based on the entities description. Both environments generate same issue.
Regards,
Cristian Colorado
Reason:
It seems for ManyToMany relationships you do not need to define a class for the "Joining Table". Therefore if I eliminate AccessLevel entity the logic would work perfectly fine. I explain further:
Explanation:
When I defined the User class I also described the relationship with Role:
#ManyToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL)
#JoinTable(name = "access_level", joinColumns = {
#JoinColumn(name = "user_id", nullable = false, updatable = false) },
inverseJoinColumns = { #JoinColumn(name = "role_id", nullable = false, updatable = false) })
private Set<Role> roles = new HashSet<Role>(0);
Important thing here is I have told hibernate that User entity will relate to Role entity through a table known as "access_level" and such table will have user_id and role_id columns in order to join previous entities.
So far this is all hibernate needs in order to work the many to many relationship, therefore when mergin it uses that information to create and tun this script:
insert into access_level (user_id, role_id) values (?, ?)
Now, the problem cames when I defined a new entity for AccessLevel:
#Entity
#Table(name="access_level")
public class AccessLevel {
#Id
#GeneratedValue
private Integer id;
#Column(name="role_id")
private Integer roleId;
#Column(name="user_id")
private Integer userId;
Now I am telling hibernate that there is a table "access_level" related to AccessLevel entity and it has 3 columns, the most important would be Id which is primary key.
So I defined "access_level" twice!
Solution:
I eliminated the Entity for access_level table.
I re-write my production script in order to have "access_level" with
user_id/role_id columns only.
Note: It would be good to know how to add a primary key to the joining table without generating issues. An alternative would be adding a composed primary key in database(user_id/role_id) which would be independient from hibernate.
Why do you need a PK column in the join table? There will be a composite PK composed of user_id and role_id. Now, as you have discovered a JoinTable for #ManyToMany will only ever have two columns and at some point you may require additional data about this relationship.
e.g.
user_id
role_id
date_granted
You may then want to use your AccessLevel entity however you replace the #ManyToMany with #OneToMany from User to AccessLevel and optionally from Role > AccessLevel.
The Hibernate docs themselves advise against #ManyToMany:
Do not use exotic association mappings:
Practical test cases for real many-to-many associations are rare. Most
of the time you need additional information stored in the "link
table". In this case, it is much better to use two one-to-many
associations to an intermediate link class. In fact, most associations
are one-to-many and many-to-one. For this reason, you should proceed
cautiously when using any other association style.
This is the code:
#Entity
public class Dept {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private Integer id;
#OneToMany(
mappedBy = "dept",
cascade = CascadeType.ALL
)
private List<Employee> employees = new ArrayList<Employee>();
public void addEmployee(Employee emp) {
this.employees.add(emp);
}
}
#Entity
public class Employee {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private Integer id;
#Column private Integer age;
public Employee(Integer a) {
this.age = a;
}
}
Then in a unit test I'm doing this (OpenJPA is used):
// ...
Dept dept = new Dept();
dept.addEmployee(new Employee(25));
this.em.persist(dept);
OpenJPA says:
Caused by: <openjpa-1.2.1-r752877:753278 nonfatal general error>
org.apache.openjpa.persistence.PersistenceException: Attempt to insert
null into a non-nullable column: column: ID table: EMPLOYEE in
statement [INSERT INTO employee (age) VALUES (?)]
{prepstmnt 841687127 INSERT INTO employee (age) VALUES (?)
[params=(int) 25]} [code=-10, state=23000]
Why ID is not auto-generated?
You one-to-many relationship is mapped as bidirectional (due to mappedBy), but I can't see the other side. Perhaps it's the cause.
And even if many-to-one side actually exists, it's not initialized in your code though it's an owning side of the relationship, therefore it specifies the state to be reflected in the database.
If you actually mean unidirectional relationship, you need to remove mappedBy.
I'm working on a hibernate entity mapping for a database view; when I do a criteria query against it, hibernate is generating bad SQL. Any help figuring out what the problem is with my mapping would be greatly appreciated!
I have two mapped entities which I am trying to grab from a database view; the view has no other columns, just the FK of each entity. One of these FK's can be treated as a primary key, since the view has a row for each primary entity. So my DB schema for the view looks like:
primary(primary_id, some_other_fields)
history(history_id, primary_id, some_other_fields)
view_latest_status_history(primary_id, history_id)
Note the view is used because I want to pick out only the latest history for each primary, not all mapped history records. Here is the object I am using for the view, with entity annotations:
#Entity
#org.hibernate.annotations.Entity(dynamicUpdate = true)
#Table(name = "view_latest_status_history")
#Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE)
public class ViewLatestStatusHistoryRow implements Serializable {
private Primary primary;
private History history;
/**
* #return Returns the history.
*/
#ManyToOne(cascade = { CascadeType.MERGE, CascadeType.PERSIST, CascadeType.REMOVE }, fetch = FetchType.LAZY)
#JoinColumn(name = "history_id", nullable = true)
#AccessType("field")
public History getHistory() {
return history;
}
//equals() and hashCode() implementations are omitted
/**
* #return Returns the primary.
*/
#Id
#ManyToOne(cascade = { CascadeType.MERGE, CascadeType.PERSIST, CascadeType.REMOVE }, fetch = FetchType.LAZY)
#JoinColumn(name = "primary_id", nullable = false)
#AccessType("field")
public Primary getPrimary() {
return primary;
}
}
Both the Primary and History objects have complete, working entity annotations.
My criteria setup:
criteria.add(Restrictions.in("primary", [collection of primary objects]));
criteria.setFetchMode("primary", FetchMode.JOIN);
criteria.setFetchMode("history", FetchMode.JOIN);
And the (wrong) generated SQL:
select this_.primary as primary78_1_, this_.primary_id as prim2_78_1_, primary2_.history_id as unique1_56_0_, ...history fields
from DB_CATALOG.dbo.view_latest_status_history this_
left outer join DB_CATALOG.dbo.history primary2_ on this_.primary_id=primary2_.primary_id
where this_.specChange in (?, ?...)
I might've mucked up a few things when editing out the specifics of our project's DB schema, but the point is the first field in the 'select' clause is wrong:
this_.primary (view_latest_status_history.primary) is not a field; the field should be called primary_id. I think this may have something to do with the #Id annotation on the primary field? Any ideas how to fix this? If I remove the #Id, I get an error telling me that the entity has no primary key.
Update:
I no longer map the view as a field using a join table notation (as suggested below). The annotations have been revised, as follows. This solution works correctly in HQL, and generates the expected schema when hbm2ddl is enabled, but I have not re-tested it using the criteria query.
#Entity
#Table(name = "view_latest_status_history")
#Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
public class ViewLatestStatusHistoryRow implements Serializable {
private String id;
private Primary primary;
private History history;
/**
* #return Returns the history.
*/
#OneToOne(optional = true)
#JoinColumn(name = "history_id", nullable = true)
#AccessType("field")
public History getHistory() {
return history;
}
//equals() and hashCode() implementations are omitted
#Id
#Column(name = "primary_id", nullable = false)
#Override
#AccessType(value = "field")
public String getId() {
return id;
}
/**
* #return Returns the primary.
*/
#PrimaryKeyJoinColumn(name = "primary_id", referencedColumnName = "unique_id")
#OneToOne(optional = false)
#AccessType("field")
public Primary getPrimary() {
return primary;
}
}
It most certainly is due to #Id annotation - primary_id is NOT a primary key in this case. Nor can you realistically have #Id and #ManyToOne on the same property.
Let me ask you this - why are you mapping ViewLatestStatusHistoryRow as an entity to begin with? It's not like you ever going to persist it. Consider mapping your latest history entry directly (as read-only) on primary (as many-to-one) and using your view as join table.