Hibernate (Spring JPA) child entities removal - java

I'm learning Hibernate (Spring) and facing strange issue with removing child entities from the parent one.
Here is what I have:
Parent entity:
#Entity
public class Company {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
Long id;
#OneToMany(cascade = CascadeType.ALL)
#JoinColumn(name = "company_id", referencedColumnName = "id")
List<CompanyObject> companyObjects;
}
Child entity:
#Entity
public class CompanyObject {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
Long id;
#Enumerated(EnumType.STRING)
ObjectType type;
#ManyToOne
#JoinColumn(name = "company_id")
Company company;
}
Here is my table definitions:
CREATE TABLE `company` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
PRIMARY KEY (`id`),
) ENGINE=InnoDB AUTO_INCREMENT=32 DEFAULT CHARSET=utf8
CREATE TABLE `company_object` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`company_id` bigint(20) NOT NULL,
`type` varchar(50) NOT NULL,
PRIMARY KEY (`id`),
KEY `FK__company` (`company_id`),
CONSTRAINT `FK__company` FOREIGN KEY (`company_id`) REFERENCES `company` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
And, also, I have the following update method:
// some code here
public void update(CompanyDto dto) {
Company company = repository.getCompanyById(companyId);
repository.save(dto.merge(company));
}
// some code here
public class CompanyDto {
private List<CompanyObjectDto> companyObjects = new ArrayList<>();
public Company merge(Company company) {
company.getCompanyObjects().clear();
for (CompanyObjectDto dto : companyObjects) {
company.getCompanyObjects().add(dto.to(company));
}
return company;
}
}
public class CompanyObjectDto {
ObjectType type;
public CompanyObject to(Company company) {
CompanyObject object = new CompanyObject();
object.setType(this.getType());
object.setCompany(company);
return object;
}
}
And as soon as I launch update method, I get the following error: java.sql.SQLWarning: Column 'company_id' cannot be null. I investigated this a little bit and found out that if I comment out company.getCompanyObjects().clear(); string it works ok, so it seems there is some problem with cascading delete action to company objects.
Could, please, somebody point me to my mistakes? Thanks.

You have mapped your entities Company and CompanyObject bidirectionally, i.e. both entities have a relation to the other entity.
In this case, there should only be one #Joincolumn and one entity must be selected as the owning entity, with the other entity marking the relation with a 'mappedby' (see http://docs.oracle.com/javaee/6/api/javax/persistence/ManyToOne.html).

You are getting error because you are removing object's from List and then use the same List as a reference to your Company object. See below code :
private List<CompanyObjectDto> companyObjects = new ArrayList<>(); //Stmt 1
Above code is used to define list which will be reference in your below code :
company.getCompanyObjects().clear(); //It will clear out all objects
for (CompanyObjectDto dto : companyObjects) { //Iterating over empty list defined in stmt 1.
company.getCompanyObjects().add(dto.to(company));
}
So your foreign key will always be null which is not permitted and throws exception.
And your code works when you comment out List#clear line because in that scenario, list already have some referenced objects which didn't modify.

Related

HIbernate Can't delete Entity with foreign key. Foreign key gets set to null

This question has been asked in many forms here but none of the solutions seem to work for me. I'm trying to delete the parent entity and I want all of the child entities to also be deleted.
My entities:
#Entity
#Table(name = "item", catalog = "myshchema")
public class Item implements java.io.Serializable {
#JoinColumn(name = "item_id", insertable = false, updatable = false, nullable = false)
#OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER, orphanRemoval = true)
private Set<ItemCategory> categories;
/* Getters and Setters and other fields*/
}
Table for Item:
CREATE TABLE `item` (
`item_id` int(11) NOT NULL AUTO_INCREMENT,
`store_id` int(11) NOT NULL,
PRIMARY KEY (`item_id`),
UNIQUE KEY `item_id_UNIQUE` (`item_id`),
KEY `FK_ITEM_STORE_ID_idx` (`store_id`),
CONSTRAINT `FK_ITEM_STORE_ID` FOREIGN KEY (`store_id`) REFERENCES `store` (`store_id`) ON DELETE NO ACTION ON UPDATE NO ACTION
) ENGINE=InnoDB AUTO_INCREMENT=84 DEFAULT CHARSET=utf8;
And my other entity
#Entity
#Table(name = "item_category", catalog = "myschema")
#IdClass(ItemCategoryIndex.class)
public class ItemCategory implements java.io.Serializable {
#Id
#Column(name = "category_id", unique = true, nullable = false, insertable = false, updatable = false)
private Integer categoryId;
#Id
private Store store;
#Id
private Item item;
#Id
private String categoryName;
/* Getters and Setters */
}
Table for ItemCategory:
CREATE TABLE `item_category` (
`category_id` int(11) NOT NULL AUTO_INCREMENT,
`store_id` int(11) NOT NULL,
`item_id` int(11) NOT NULL,
`category_name` varchar(45) NOT NULL,
PRIMARY KEY (`category_id`),
UNIQUE KEY `category_id_UNIQUE` (`category_id`),
UNIQUE KEY `IDX_UNIQUE_STORE_CATEGORY` (`store_id`,`item_id`,`category_name`) USING BTREE,
KEY `FK_CATEGORY_STORE_ID_idx` (`store_id`),
KEY `FK_ITEM_CATEGORY_ID_idx` (`item_id`),
CONSTRAINT `FK_CATEGORY_STORE_ID` FOREIGN KEY (`store_id`) REFERENCES `store` (`store_id`) ON DELETE NO ACTION ON UPDATE NO ACTION,
CONSTRAINT `FK_ITEM_CATEGORY_ID` FOREIGN KEY (`item_id`) REFERENCES `item` (`item_id`) ON DELETE CASCADE ON UPDATE NO ACTION
) ENGINE=InnoDB AUTO_INCREMENT=162 DEFAULT CHARSET=utf8;
I try to delete the item like this:
Item item = entityManager.find(Item.class, idList.get(i));
entityManager.remove(item);
My logs show that Hibernate is trying to set the primary key for ItemCategory to null:
Hibernate: update myschema.item_category set item_id=null where item_id=?
ERROR o.h.e.jdbc.spi.SqlExceptionHelper.logExceptions 146 - Column 'item_id' cannot be null
I even tried looping through the child records and deleting them manually, but Hibernate still issues this update to null query. What am I doing wrong?
I have to break your problem down to two parts
First - let's talk about your database schema design.
According to your schema, item and item_category has a one-to-many relationship meaning an item can have/be-assigned-to different categories but different items cannot have/be-assigned-to the same category.
That is totally fine if it is indeed your business requirement, I mention it because it does not make sense to me and this circumstance rarely happens.
If what you want is that a category can have multiple items and vice versa, itemand item_category must be a many-to-many relationship. There should be a join table additionally.
Second - let's say the schema don't change
ItemCategory is the owner of the relationship because it has a foreign key item_id refering to item table. So the ItemCategoy should look roughly like this:
#Entity
#Table(name = "item_category")
public class ItemCategory {
#Id
private Integer categoryId;
private Store store;
#ManyToOne
#JoinColumn(name="item_id", /*cascade = ...*/)
private Item item;
private String categoryName;
/* Getters and Setters */
}
Your Item entity will be roughly like this:
#Entity
#Table(name = "item", catalog = "myshchema")
public class Item implements java.io.Serializable {
#OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER, orphanRemoval = true, mappedBy="item")
private Set<ItemCategory> categories; //`mappedBy`used here because this entity is not the owner of the relationship according to what mentioned above
/* Getters and Setters and other fields*/
}
To remove all the child entities(ItemCategory) from Item , simply
em.remove(item);
The orphanRemoval is true, deleting the parent, the children will be deleted as well.
In Hibernate, you need to decide who is owning the relationship. If you have the parent side (ItemCategory) owning the relationship, you will find insertion/deletion of Item+ ItemCategory will involve update of item_id in ItemCategory table (which is what I observed from your exception). In most case it is not preferable. We usually let the children own the relationship. This is done by using mappedBy
(pseudo-code)
class Item {
//...
#OneToMany(mappedBy = "item", cascade=ALL, orphanRemoval=true)
private Set<ItemCategory> categories;
}
class ItemCategory {
//...
#ManyToOne
#JoinColumn(name="item_id")
Item item;
}
The trick here is mappedBy

Hibernate Many-To-One Relationship without Foreign Key but with reverse foreign key

I have the following DB:
CREATE TABLE car_owner (
car_owner_id int(11) NOT NULL,
car_id_fk int(11) DEFAULT NULL,
PRIMARY KEY (car_owner_id),
KEY car_owner_car_fk_idx (car_id_fk),
CONSTRAINT car_owner_car_fk FOREIGN KEY (car_id_fk) REFERENCES car (car_id) ON DELETE NO ACTION ON UPDATE NO ACTION,
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
CREATE TABLE car (
car_id int(11) NOT NULL AUTO_INCREMENT,
car_type varchar(45) DEFAULT NULL,
car_plates varchar(25) DEFAULT NULL,
PRIMARY KEY (car_id),
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=latin1;
And In the java model:
For CarOwner I have:
#Entity
#Table(name="car_owner")
#NamedQuery(name="CarOwner.findAll", query="SELECT co FROM CarOwner co")
public class CarOwner implements Serializable {
#Id
#GeneratedValue
#Column(name="car_owner_id")
private Integer carOwnerId;
.....
//bi-directional many-to-one association to Car
#OneToMany(fetch = FetchType.EAGER)
#JoinColumn(name = "car_id_fk", referencedColumnName = "car_id")
private List<Car> cars;
And for Car:
#Entity
#Table(name="car")
#NamedQuery(name="Car.findAll", query="SELECT c FROM Car c")
public class Car implements Serializable {
#Id
#GeneratedValue
#Column(name="car_id")
private Integer carId;
......
//bi-directional many-to-one association to car_owner
#ManyToOne(fetch=FetchType.EAGER)
#JoinColumn(name = "car_owner_id")
private CarOwner carOwner;
The problem here is that Hibernate can't relate the tables and goes creating new car_id and car_owner_id columns in car table automatically.
Can anybody help in finding the right combination in the model to relate the tables appropriately.
#JoinColumn should be in owner of relationship (in a one to many it's the many side that's regarded the owner).
So I will modify this in car
#ManyToOne(fetch=FetchType.EAGER)
#JoinColumn(name = "car_owner_id",insertable=false, updatable=false)
private CarOwner carOwner;
And this in CarOwner
#OneToMany(fetch = FetchType.EAGER,mappedBy = "carOwner")
private List<Car> cars;
As a side note I would also not be using EAGER but that has got nothing to do with question.
Both tables knowing about each other is called Bi-directional relationship.This happens when each table has a key to other table. This is what your java code is expecting.Your tables in database however have a uni-directional relationship. Meaning one table knows about the other but not both. Your car_owner knows about car because of foriegn key CONSTRAINT car_owner_car_fk FOREIGN KEY but your car does not have any idea about car_owner both are perfectly valid.
Now the problem is that in your Java code you are treating it as a bi-directional relationship.
//bi-directional many-to-one association to car_owner
#ManyToOne(fetch=FetchType.EAGER)
#JoinColumn(name = "car_owner_id")
private CarOwner carOwner;
but car does not have car_owner_id why are you treating it a Bi-directional relationship.
Now either update database to make them bi or change java code.

Spring JPA/Hibernate #ManyToOne relationship always inserting child row

I'm attempting to use Spring JPA/Hibernate to maintain a Parent table with a child table related based on an Id. If I attempt to insert the same object twice, the Parent table gets "updated" properly, but the child table is always inserted to even though the entry in the child table is already present.
CREATE TABLE `parent_table` (
`parent_id` int(11) NOT NULL AUTO_INCREMENT,
`ex_column_1` int(11) NOT NULL, // there is a unique constraint on this field - I just didn't include it
`ex_column_2` int(11) NOT NULL,
`child_id` int(11) NOT NULL,
PRIMARY KEY (`parent_id`),
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
Child Table:
CREATE TABLE `child_table` (
`child_id` int(11) NOT NULL AUTO_INCREMENT,
`child_col_1` varchar(200) DEFAULT NULL,
PRIMARY KEY (`child_id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
POJOs:
#Entity
#Table(name = "parent_table")
public final class Parent {
#GeneratedValue(strategy = GenerationType.AUTO)
#Column(name = "parent_id")
private long id;
#Id
#Column(name = "ex_column_1")
private int exampleField;
#Column(name = "ex_column_2")
private int exampleField2;
#ManyToOne(cascade = CascadeType.ALL)
#JoinColumn(name = "child_id", unique = true)
private Child c;
public Parent(/*params*/) {}
// getters ...
#Entity
#Table(name = "child_table")
public static final class Child {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#Column(name = "child_id")
private long id;
#Column(name = "child_col_1")
private exampleChildField;
public Child(/*params*/) {}
// getters ...
}
}
Actual example of how POJO's are constructed and saved:
#RestController
#RequestMapping("/parents")
public final class ParentController {
private final ParentRepository parentRepository;
#Inject
public ParentController(final ParentRepository parentRepository) {
parentRepository = parentRepository;
}
#RequestMapping(value = "", method=RequestMethod.PUT)
public void updateParents(#RequestBody Parent[] parents) {
// ignore input for now, test by constructing object
Parent.Child c = new Parent.Child(0L, "test 1");
Parent p = new Parent(0L, "unique_column_1", "column_2", c);
Set<Parent> pSet = new HashSet<>();
pSet.add(p);
parentRepository.save(pSet);
}
}
Repository Layer:
public interface ParentRepository extends CrudRepository<Parent, Long>, ParentRepositoryCustom {}
public interface ParentRepositoryCustom {
void save(Set<Parent> parents);
}
#Repository
final class ParentRepositoryImpl implements ParentRepositoryCustom {
private final EntityManager entityManager;
#Inject
public EmployerRepositoryImpl(final EntityManager entityManager) {
this.entityManager = entityManager;
}
#Override
#Transactional
public void save(Set<Parent> parents) {
parents.forEach(parent -> {
Session session = (Session) entityManager.getDelegate();
session.saveOrUpdate(parent);
});
}
}
If the entry in either table doesn't exist, it persists both entities just fine and links the proper Ids between tables. The issue occurs if the same parent and child object are inserted again. The UPDATE occurs on the Parent table, but an INSERT on the child:
Hibernate: insert into child_table (child_col_1) values (?)
Hibernate: insert into parent_table (ex_column_1, ex_column_2, child_id) values (?, ?, ?)
On second insert:
Hibernate: insert into child_table (child_col_1) values (?)
Hibernate: update parent_table set ex_column_2=?, child_id=? where ex_column_1=?
How do I get EntityManager to persist these correctly if the entry already exists?
I think I've found issue. The client that is used to post new "parent" objects to the rest interface, by default, sets the primary key ids to 0. This works for the parent because ex_column_1 is an ID that's known at the time objects get created by some other mechanism.
On the other hand, the Child object only has a primary key as its Id and by setting it to zero, Spring attempts to find a related child row based on an id of zero, and always does an insert instead of an update if a child row already exists. Doing a simple check using the EntityManager.find() method on both objects then saving or merging fixed it.

How to map two tables to one entity using foreign-key?

I have a problem very similar to this: How do I join tables on non-primary key columns in secondary tables?
But I'm not sure if I can apply the same solution.
I have two tables like these:
CREATE TABLE CUSTOMER
(
CUSTOMER_ID INTEGER NOT NULL,
DETAIL_ID INTEGER NOT NULL,
PRIMARY KEY( CUSTOMER_ID ),
CONSTRAINT cust_fk FOREIGN KEY( DETAIL_ID ) REFERENCES DETAILS( DETAIL_ID )
)
CREATE TABLE DETAILS
(
DETAIL_ID INTEGER NOT NULL,
OTHER INTEGER NOT NULL,
PRIMARY KEY( DETAIL_ID )
)
I'd like to map these tables to a single class called Customer, so I have:
#Entity
#Table(name = "CUSTOMERS")
#SecondaryTable(name = "DETAILS", pkJoinColumns=#PrimaryKeyJoinColumn(name="DETAIL_ID"))
public class Customer {
#Id
#GeneratedValue
#Column(name = "CUSTOMER_ID")
private Integer id;
#Column(table = "DETAILS", name = "OTHER")
private Integer notes;
// ...
}
but this works only if DETAIL_ID matches CUSTOMER_ID in the primary table.
So my question is: how can i use a foreign-key field in my primary table to join on the primary-key of the secondary table?
UPDATE
I tried to set:
#SecondaryTable(name = "DETAILS", pkJoinColumns=#PrimaryKeyJoinColumn(name="DETAIL_ID", referencedColumnName="DETAIL_ID"))
but when I run the application I get this exception:
org.hibernate.MappingException: Unable to find column with logical name: DETAIL_ID in org.hibernate.mapping.Table(CUSTOMERS) and its related supertables and secondary tables
For anyone looking for an answer to this, using #SecondaryTable is not the way to join two tables with non-primary key columns, because Hibernate will try to assosiate the two tables by their primary keys by default; you have to use #OneToMany review http://viralpatel.net/blogs/hibernate-one-to-many-annotation-tutorial/ for a solution, here's a code snippet in case that url stops working:
Customer Class:
#Entity
#Table(name="CUSTOMERS")
public class Customer {
#Id
#GeneratedValue
#Column(name="CUSTOMER_ID")
private Integer id;
#ManyToOne
#JoinColumn(name="DETAIL_ID")
private Details details;
// Getter and Setter methods...
}
Details Class:
#Entity
#Table(name="DETAILS")
public class Details {
#Id
#GeneratedValue
#Column(name="DETAIL_ID")
private int detailId;
#Column(name="OTHER")
private String other;
#OneToMany(mappedBy="details")
private Set<Customer> customers;
// Getter and Setter methods...
}
This is easily accessible through hibernate with the following code:
Session session = HibernateUtil.getSessionFactory().openSession();
Query query = session.createQuery("select id, details.other from Customer");
I hope this helps anyone out there spending hours searching for a way to achieve this like I did.
You can use the referenceColumnName attribute of the #PrimaryKeyJoinColumn annotation to define the join column to the referenced table. In fact, by combining use of name/referencedColumnName you can join on arbitrary on both sides, with the constraint that if duplicates are found your ORM provider will throw an exception.

Master/Detail relationship with auto increment. Is it possible to persist them at once?

I have this scenario :
tbl_master(
master_field_pk int(11) NOT NULL AUTO_INCREMENT,
PRIMARY KEY (`master_field_pk`)
)
and
tbl_detail(
`detail_field_pk` int(11) NOT NULL AUTO_INCREMENT,
`master_field_pk` int(11) DEFAULT NULL,
CONSTRAINT `child_fk1` FOREIGN KEY (`master_field_pk`) REFERENCES `tbl_master` (`master_field_pk`) ON DELETE NO ACTION ON UPDATE NO ACTION,
PRIMARY KEY (`master_field_pk`)
)
This is my hibernate class for master :
#Entity
#Table(name = "tbl_master")
#Name("TblMaster")
public class TblMaster implements Serializable{
#Id
#Column(name = "master_field_pk")
#GeneratedValue(strategy = GenerationType.AUTO)
private Integer MasterFieldPk;
#Cascade({CascadeType.ALL,CascadeType.DELETE_ORPHAN})
#OneToMany(mappedBy="tblMaster")
#JoinColumn(name="master_field_pk", insertable=true, updatable=true, referencedColumnName="master_field_pk")
private Set<TblDetail> tblDetails;
#Transient
private List<TblDetail> tblDetailsList;
// any other things
}
and this one is for detail :
#Entity
#Table(name="tbl_detail")
#Name("TblDetail")
public class TblDetail implements Serializable{
#Id
#GeneratedValue(strategy=GenerationType.AUTO)
#Column(name="detail_field_pk")
private Integer detailFieldPk;
#Column(name="master_field_pk")
private Integer MasterFieldPk;
#ManyToOne(fetch=FetchType.LAZY)
#JoinColumn(name="master_field_pk", insertable = false, updatable = false, referencedColumnName = "master_field_pk")
private TblMaster tblMaster;
// any other things
}
To insert, i use this block of code (more or less) :
TblDetail detail_1 = ...
detail_1.setTblMaster(tblMaster);
// and so on ..
Set<TblDetails> set = ...
set.add(detail_1);
// and so on...
tblMaster.setTblDetails(set);
em.persist(tblMaster);
em.flush();
All the records (the master record and the details record) are perfectly inserted. However, the master_field_pk in tbl_detail - the foreign key - is still null. Is it possible to avoid this behavior? Thanks
No. You have to persist the master first. The detail rows need to know what the master ID is to be linked.
This can be done but only when the key is assigned by the application, for example GUID keys assigned by the application, or some sort of key server.
CascadeType.ALL should do the trick for you. It has worked for me all the time.
I think your problem lies in your #GeneratedValue attribute strategy. You should be using GenerationType.IDENTITY instead of GenerationType.AUTO.

Categories