Delete a referenced/OneToMany relation instead of "Nulling" the column - java

We have two entities:
public class User {
#OneToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL)
#JoinColumn(name = "user_id", insertable = true, updatable = true)
#ElementList(name = "tabledata")
public List<MyUserTableData> tableData = new ArrayList<MyUserTableData>();
}
public class MyUserTableData {
public Long user_id;
}
The action that I do is that I remove an entry from u.tableData and then call the EntityManager to merge(u).
OpenJPA will remove the entry from the User object by setting the corresponding record in the MyUserTableData with a user_id = "null".
But what I want is that if the entry is deleted from the User, it should also delete the record from the MyUserTableData and not just NULL the column user_id.
How can I force OpenJPA to delete the OneToMany related entry instead of putting a null in the column?
I will not accept answers that do asume that my database schema is bad :) The table MyUserTableData is basically a foreign key table that connects the user to another entity but holds some more information then just a foreign key, it adds some meta data to the foreign key that neither belong to the user nor to the other entity.
Thanks!
Sebastian

I was able to resolve my issue:
http://openjpa.apache.org/builds/1.0.4/apache-openjpa-1.0.4/docs/manual/manual.html#dependent
#ElementDependent => does exactly what I want.

Related

SpringBoot - Entity propagation for relationships

I have two entities: WorkoutTemplate and ConcreteExercise.
In WorkoutTemplate I have this relationship with ConcreteExercises
#OneToMany(cascade = CascadeType.ALL, mappedBy = "belongingWorkout", fetch = FetchType.LAZY)
private List<ConcreteExercise> concreteExercises;
And in ConcreteExercise I have this relationship with WorkoutTemplate
#ManyToOne
private WorkoutTemplate belongingWorkout;
I would like to insert a WorkoutTemplate into my database...
I make the request to the controller sending a Json like this:
{ "workoutName" : "My Workout",
concreteExercises: [
{
"name" : "Squat"
}
]
}
The DAO insert into my DB the WorkoutTemplate
And insert also in the table of the ConcreteExercise the name.
But not the reference to the WorkoutTemplate...
Practically, the table ConcreteExercise is made of:
id, name, belongin_workout_id
With the request above, we populate the id (auto-increment) and the name, but not the foreign key to the WorkoutTemplate.
How can I solve this ?
I would like to automatically insert the foreign key without sending it in the request or doing it manually into the service
Hi there it's because you are not using #JoinColumn which marks a column for as a join column for an entity association or an element collection.
On your WorkoutTemplate entity - you can retain this:
#OneToMany(cascade = CascadeType.ALL, mappedBy = "belongingWorkout", fetch = FetchType.LAZY)
private List<ConcreteExercise> concreteExercises;
But on your ConcreteExercise entity - you need to have this change:
#ManyToOne
#JoinColumn(name = "workout_template_id", nullable = false)
private WorkoutTemplate belongingWorkout;
The above code will create a foreign key linking the ConcreteExercise entity with the primary key from the WorkoutTemplate entity. The name of the foreign key column in the ConcreteExercise entity is specified by name property which for this case is workout_template_id - feel free to change this.
If you have set this up but still not working, it will also be helpful to share a code snippet on how are you saving these.

Why isn't the foreign key field of a Many-to-One relationship being set on insert?

My Spring web application allows users to update "Employee" records to change the fields or add new "Phone" records related to this "Employee" record. However, when the "Employee" record is submitted for update after adding a new "Phone" record, it's throwing a SQL error exception.
The problem is that the "employee_id" foreign key on "Phone" table to the "Employee" table isn't set in the eventual SQL insert statement submitted to the database. However, in the "PhoneEntity" JPA entity object that is referenced by the updated/merged "EmployeeEntity" object, the property associated with the employee_id database field isn't null, it's set to the "EmployeeEnity" Object being updated/merged.
From my understanding of JPA, having the entity property associated with a database field should set it when the insert statement for the entity's record is submitted to the database, but in this case it isn't which is causing this error.
I've tried stepping through with a debugger, and I have verified that the created PhoneEntity object is a member of EmployeeEntity's phones property, and that the same PhoneEntity's employee property is set to the same EmployeeEntity object (with the same object IDs) in a bidirectional relationship.
I've also set the hibernate.show_sql=true to see the SQL statement being submitted to the database and it includes the statement (with the ellipses being more fields):
Hibernate:
insert
into
phone
(id, employee_id, ...)
values
(?, ?, ...)
Which means that it is inserting a new phone for the new PhoneEntity object.
After trying to running this insert statement it gives the SQL error "Column 'employee_id' cannot be null". However like I said before, I've checked with the debugger and the employee property is indeed set to the EmployeeEntity object.
this is a simplified example of what my code looks like:
#Entity
#Table(name = "employee")
public class EmployeeEntity implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "id", unique = true, nullable = false)
private Integer id;
#OneToMany(mappedBy="employee", cascade = {CascadeType.PERSIST})
private Set<PhoneEntity> phones = new HashSet<>();
...
}
#Entity
#Table(name = "phone")
public class PhoneEntity implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "id", unique = true, nullable = false)
private Integer id;
#ManyToOne
#JoinColumn(name = "employee_id", nullable = false)
private EmployeeEntity employee;
...
}
With tables that have the structure created by the following SQL statements.
CREATE TABLE employee (
id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
...
);
CREATE TABLE phone (
id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
employee_id INT NOT NULL,
...
FOREIGN KEY(employee_id) REFERENCES employee(id)
);
And the following is where it actually submits the updates to the entity manager to make updates to the database.
public void update(EmployeeDomain employee) {
EmployeeEntity entity = employeeDomainToEntity.transform(employee)
getEntityManager().merge(entity);
}
The EmployeeEntity and PhoneEntity objects are created by converting similar domain objects that were in turn deserialized from a http request. I'd include more of this section of the code but, as I've mentioned, I've already confirmed with my debugger that the actual entity objects being submitted to the merge are already in the form that we expected with the phones fields and employee fields being set correctly, so the end entities should be correct.
In the official JPA specification document (version 2.1) in section "3.2.7.1 Merging Detached Entity State" (page 85) we find:
For all entities Y referenced by relationships from X having the cascade element value cascade=MERGE or cascade=ALL, Y is merged recursively as Y'. For all such Y referenced by X, X' is set to reference Y'. (Note that if X is managed then X is the same object as X'.)
This explains that you are lacking cascade=MERGE for the annotation of the phones field.
As proposed in thanh ngo's answer, the aforementioned definition (or: explanation) thus translates to:
#OneToMany(mappedBy="employee", cascade = {CascadeType.PERSIST, CascadeType.MERGE})
private Set<PhoneEntity> phones = new HashSet<>();
Alternatively, you could also make use of cascade=CascadeType.ALL. However, this would also include operations such as CascadeType.REMOVE which might not always be intended.
Hope it helps.
I think the problem is that you are using merge.
The cascade type setting for the entity should be:
#OneToMany(mappedBy="employee", cascade = {CascadeType.PERSIST, CascadeType.MERGE})
private Set<PhoneEntity> phones = new HashSet<>();

Spring JPA OneToOne FK as PK Cascade.Remove

I've got two tables, b and a:
they have a one-to-one bidirectional relationship
a has a foreign key to b that defines this relationship
this foreign key is also considered as a primary key for a, and a JPA #ID
I want a cascade removal that deletes the related b when a is deleted
in MySQL, a's b_id is NOT NULL
The problem is that when I delete my A object with JPA repository, I get a ConstraintViolationException on its foreign key.
I would expect that both a and b rows are deleted (cleverly starting with a's one).
How could I work around this knowing that I want to keep:
my DB schema the same
the cascade removal from a to b
the b id being the JPA #Id for a
CREATE TABLE `b` (
`dbid` int(11) NOT NULL AUTO_INCREMENT,
PRIMARY KEY (`dbid`),
);
CREATE TABLE `a` (
`b_id` int(11) NOT NULL,
KEY `b_fk` (`b_id`),
CONSTRAINT `b_fk` FOREIGN KEY (`b_id`) REFERENCES `b` (`dbid`),
);
#Entity
#Table(name = "a")
public class A {
#Id
#Column(name = "b_id")
#GeneratedValue(generator = "gen")
#GenericGenerator(name = "gen", strategy = "foreign", parameters = #Parameter(name="property", value="b"))
private Integer bId;
#OneToOne(cascade = CascadeType.REMOVE)
#PrimaryKeyJoinColumn
private B b;
}
#Entity
#Table(name = "b")
public class B {
#Id
#GeneratedValue(strategy= GenerationType.IDENTITY)
#Column(name = "dbid")
private Integer id;
#OneToOne(mappedBy = "b")
private A a;
}
[EDIT] After all discussions in answer comments and re-reading my question, the proposals with orphanRemoval indeed are in scope and work.
If you want to delete object of B, whenever the associated A is deleted (it's the fourt point of your wishlist:
I want a cascade removal that deletes the related b when a is deleted
then you need to change your mapping in A to:
#OneToOne(cascade = CascadeType.REMOVE, orphanRemoval = true)
#PrimaryKeyJoinColumn
private B b;
In terms of just the MySQL side of your implementation, the records in table B have no 'knowledge' of any record in table A. In the database the relationship is unidirectional
The native cascade functionality exists to prevent foreign key errors, by telling the DB what to do when deleting a record would leave a foreign key pointing nowhere. Deleting a table A record would not cause a foreign key error in any table B records, so any native cascade functionality would not be triggered
To reiterate; You cannot keep the schema the same, and the cascade removal from a to b, because you don't actually have the cascade removal from a to b
You also mentioned in the comments that some table B records can exist without a table A records which isn't in the original question
To obtain the automatic deletion of table B records you describe, you have a few options with regards to the DB:
Swap the relation over - Remove the current foreign key and add a nullable foreign key column in table B that references the primary key of table A. You can then put a cascade delete on this foreign key. Keep the new column null for the table B records that do not 'belong' to a table A record. You could also add a unique index to this column to secure a one to one relationship
Add a DB trigger - On deletion of a table A record, add a DB trigger that removes the referenced table B record
Add a DB procedure - Add a procedure that deletes a table A record and then the referenced table B record in turn, probably within a transaction. Going forwards, only delete table A records using the procedure
Don't solve the problem at the DB level - Basically the same as option 3, but move the procedure logic out of the DB layer into the application logic
There may be something in JPA that solves your dilemma out of the box, but under the hood it will be doing one of the above (not option 1 and probably option 4)
In order to achieve what you have asked, I have tweaked your tables as follows:
CREATE TABLE b (
dbid INT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY
);
CREATE TABLE a (
b_id int(11) NOT NULL PRIMARY KEY REFERENCES b(dbid) ON DELETE CASCADE
);
CASCADE DELETE wasn't added in your DDL.
This will enable cascade delete. To delete the b record on deletion of a I made following changes in class A:
#Entity
#Table(name = "a")
public class A {
#Id
#Column(name = "b_id")
#GeneratedValue(generator = "gen")
#GenericGenerator(name = "gen", strategy = "foreign", parameters = #Parameter(name="property", value="b"))
private Integer bId;
#OneToOne(cascade = CascadeType.REMOVE, orphanRemoval = true)
#PrimaryKeyJoinColumn
private B b;
}
Find link here to the working solution.
Can you try in class B to add the following
#OneToOne(mappedBy = "b", cascade = CascadeType.REMOVE)
private A a;
In addition, if in the database you have only a foreign key "a has a foreign key to b" can you also make a foreign key from b to a as well.
#OneToOne(mappedBy = "b",cascade = CascadeType.ALL,fetch = FetchType.LAZY,orphanRemoval=true )
private A a;

Hibernate Exception: Missing Column (column exists)

Okay, so within the database we have a table called distributionCompanies, created like so:
CREATE TABLE `distributionCompanies` (
`distributionCompanyID` INT(11) NOT NULL,
`distributionCompanyName` VARCHAR(255) NOT NULL,
PRIMARY KEY (distributionCompanyID)
);
I'm trying to map this table to a class using Hibernate:
#Entity
#Table(name = "distributionCompanies")
public class DistributionCompany implements DatabaseObject {
#Id
#GeneratedValue
#Column(name = "distributionCompanyID", length = 11, unique = true, nullable = false)
private int distributionCompanyID;
....
However, when running, I hit this issue:
Initial SessionFactory creation failedorg.hibernate.HibernateException: Missing column: distributionCompanyID_distributionCompanyID in database2.distributionCompanies
This isn't the only table in the database, and I've managed to map other classes successfully using the same method, so I'm a little stumped as to why this is causing an issue.
Thank you for your time,
Samuel Smith
EDIT: In response to Xavi's comment, I temporarily removed another mapping for the column, and the error went away, so the bad-egg probably lays in the following code:
#ManyToOne(targetEntity = DistributionCompany.class)
#JoinTable(name = "distributionCompanies", joinColumns = { #JoinColumn(name = "distributionCompanyID", nullable = false) })
private int distributionCompanyID;
Hibernate is looking for a column named distributionCompanyID_distributionCompanyID in your distributionCompanies table.
This is probably due to a ToOne association mapping towards this table without #JoinColum.
From Hibernate Documentation:
The #JoinColumn attribute is optional, the default value(s) is like in one to one, the concatenation of the name of the relationship in the owner side, _ (underscore), and the name of the primary key column in the owned side. In this example company_id because the property name is company and the column id of Company is id.
If you've got a #ManyToOne or #OneToOne association mapping in another entity, this would explain why Hibernate is looking for such a column.
EDIT Seeing the association mapping you posted, it looks like it should be:
#ManyToOne(targetEntity = DistributionCompany.class)
#JoinColumn(name = "distributionCompanyID")
private DistributionCompany distributionCompany;
The #JoinTable annotation is used to specify a join table (that means an intermediate table used to model many-to-many associations). And the point of mapping an association would be to dispose of the mapped object instance (in this case a DistributionCompany, not just a distributionCompanyId).

strange sql behavior when do deleting relation for many-to-many in JPA

This is a question about Hibernate's generated sql about deleting one relationship under many-to-many mapping, not 'cascade' problem.
I use JPA 2 and hibernate as its implementation.
I have two models, User and Role. One user can have many role, and one role can have many users, so they are many-to-many mapping:
#Entity
class User{
#Id Long id;
#ManyToMany(fetch = FetchType.LAZY, cascade = CascadeType.REFRESH)
#JoinTable(name = "user_role", inverseJoinColumns = #JoinColumn(name = "role_id"),
joinColumns = #JoinColumn(name = "user_id"))
private List<Role> roles;
}
#Entity
class Role{
#Id Long id;
#ManyToMany(fetch = FetchType.LAZY,cascade = CascadeType.REFRESH, mappedBy = "roles")
private List<User> users;
}
and the mapping works well , hibernate auto create three tables for this mapping
table user
table role
table user_role
Now here is the problem, what I want is just remove one role from one user (not remove a user or a role, just one relation between one user and one role, means only need remove one record from the table user_role). Here is the code I tried:
public void removeOneRoleFromUser(long userId, long roleId){
User user = userService.getById(userId);
Role role = roleService.getById(roleId);
user.getRoles().remove(role); //here
userService.update(user);
}
when I execute this code it work, the role was removed the from the user indeed. But when I check the sql which hibernate generated for it, it's not what I expected, The hibernate generated sql is:
delete from user_role where user_id = {userId}
insert into user_role values({user_id}, {role_id_not_removed})
...
insert into user_role values({user_id}, {another_role_id_not_removed})
So for deleting one role from one user, hibernate first delete all roles from the user, then add those role which should not be removed back to the user one by one.
And what I expect is just one sql sentence archive it:
delete from user_role where user_id = {userId} and role_id = {role_id}
I know there is some other ways I can archive this like introducing another entity UserRoleMapping which mapping to the table user_role, then directly remove one UserRoleMapping instance will remove one role from one user; but I want to know is there any solution I can get the expect with the current solution.
I've not checked that this explanation is true, but it has good points.
Without any index column, a List is in fact a bag: no order, and duplicates allowed. So Hibernate considers it possible that you have the same role twice in the list of roles of a user.
So issuing delete from user_role where user_id = ? and role_id = ? is not possible because it would potentially remove several roles instead of just the one you removed from the list.
Try adding an index column, or using a Set<Role> instead of a List<Role>.

Categories

Resources