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.
Related
I have two tables, these are profile_info and user_Info.
Database ER-diagram
Table creation:
CREATE TABLE `user_info` (
`id` int NOT NULL AUTO_INCREMENT,
`userLogin` varchar(20) DEFAULT NULL,
`userPassword` varchar(60) DEFAULT NULL,
`userType` enum('user','administrator') DEFAULT NULL,
`userEmail` varchar(320) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `userEmail` (`userEmail`)
);
CREATE TABLE `profile_info` (
`id` int NOT NULL AUTO_INCREMENT,
`userId` int DEFAULT NULL,
`userRegistrationDate` datetime DEFAULT NULL,
`userFirstName` varchar(25) DEFAULT NULL,
`userSurName` varchar(25) DEFAULT NULL,
`accountBalance` double DEFAULT '0',
PRIMARY KEY (`id`),
UNIQUE KEY `userUniqueId` (`userId`),
CONSTRAINT `fkUserId` FOREIGN KEY (`userId`) REFERENCES `user_info` (`id`) ON DELETE CASCADE,
CONSTRAINT `chk_accountBalance` CHECK ((`accountBalance` >= 0))
);
So I have one to one relationship(one profile is related only to one user)
Here are my entities :
User :
#Table("user_info")
#Data
#AllArgsConstructor
#NoArgsConstructor
public class User implements Persistable<Integer> {
#Id
#Column("id")
private Integer id;
#Column("userLogin")
private String userLogin;
#Column("userPassword")
private String userPassword;
#Column("userType")
private String userType;
#Column("userEmail")
private String userEmail;
#Override
public boolean isNew() {
return id==null;
}
}
Profile :
#Table("profile_info")
#Data
#AllArgsConstructor
#NoArgsConstructor
public class Profile implements Persistable<Integer> {
#Id
#Column("id")
private Integer id;
#Column("userRegistrationDate")
private LocalDateTime userRegistrationDate;
#Column("userFirstName")
private String userFirstName;
#Column("userSurName")
private String userSurName;
#Column("accountBalance")
private double accountBalance;
#MappedCollection(idColumn = "id")
private User user;
#Override
public boolean isNew() {
return id==null;
}
}
I use standard repositories as CRUDRepository
UserRepository:
#Repository
public interface UserRepository extends CrudRepository<User,Long> {
}
ProfileRepository:
#Repository
public interface ProfileRepository extends CrudRepository<Profile,Integer> {
}
In user table I have only two records :
In profile table I have only one which I'm gonna update
And I'm trying to insert User entity first, and after that update Profile entity
public static void main(String[] args) {
ApplicationContext context = new AnnotationConfigApplicationContext(ApplicationConfig.class);
ProfileRepository repository = context.getBean("profileRepository",ProfileRepository.class);
UserRepository userRepository =context.getBean("userRepository",UserRepository.class);
User user = new User(null,"Login","newUserP","user","TestEmail");
userRepository.save(user);
user.setUserLogin("new bqqvnbvnvbn");
System.out.println(user.getId());
Profile profile = new Profile(34, LocalDateTime.now(),"fff","sss",250.0,user);
repository.save(profile);
}
User id is :
User was succesfully created
But When I'm trying to update Profile I'm getting error duplicate entry
Caused by: java.sql.SQLIntegrityConstraintViolationException: Duplicate entry '77' for key 'user_info.PRIMARY'
at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:117)
at com.mysql.cj.jdbc.exceptions.SQLExceptionsMapping.translateException(SQLExceptionsMapping.java:122)
at com.mysql.cj.jdbc.ClientPreparedStatement.executeInternal(ClientPreparedStatement.java:953)
at com.mysql.cj.jdbc.ClientPreparedStatement.executeUpdateInternal(ClientPreparedStatement.java:1092)
at com.mysql.cj.jdbc.ClientPreparedStatement.executeUpdateInternal(ClientPreparedStatement.java:1040)
at com.mysql.cj.jdbc.ClientPreparedStatement.executeLargeUpdate(ClientPreparedStatement.java:1350)
at com.mysql.cj.jdbc.ClientPreparedStatement.executeUpdate(ClientPreparedStatement.java:1025)
at org.springframework.jdbc.core.JdbcTemplate.lambda$update$2(JdbcTemplate.java:965)
at org.springframework.jdbc.core.JdbcTemplate.execute(JdbcTemplate.java:651)
... 38 more
But in Spring documentation is said "If the aggregate root is not new, all referenced entities get deleted, the aggregate root gets updated, and all referenced entities get inserted again. Note that whether an instance is new is part of the instance’s state."
So it should delete user from database first and after that update profile and insert deleted user
Also, when I did this I got this exception
Exception in thread "main" org.springframework.data.relational.core.conversion.DbActionExecutionException: Failed to execute DbAction.Insert(entity=User(id=77, userLogin=new bqqvnbvnvbn, userPassword=newUserP, userType=user, userEmail=TestEmail), propertyPath=user, dependingOn=DbAction.UpdateRoot(entity=Profile(id=34, userRegistrationDate=2021-08-15T20:38:16.862, userFirstName=fff, userSurName=sss, accountBalance=250.0, user=User(id=77, userLogin=new bqqvnbvnvbn, userPassword=newUserP, userType=user, userEmail=TestEmail))), qualifiers={})
You have repositories for both User and Profile.
This means both are Aggregate Roots.
Aggregate Roots must only reference each other by id. So Profile.user should be of type Integer.
Alternatively you might decide that the two entities really form an aggregate. In that case you must:
Decide which entity is the Aggregate Root.
Remove the repository for the non root entity.
You may (should) also remove the id for the non root entity.
See also https://spring.io/blog/2018/09/24/spring-data-jdbc-references-and-aggregates
Note: Implementing Persistable as you did is pretty much superfluous because it replicates the default behaviour anyway.
Another note: #MappedCollection(idColumn = "id") is ignored for all simple references. It applies only to collections like List, Set or Map.
I want to get an object of EventDate using primary key. Following is the query i'm executing
EventData eventData = entityManager.find(EventData.class, eventdataid);
After executing this command in console i'm getting the query as
select eventsajgj0_.FILE_ID as FILE_ID8_14_0_, eventsajgj0_.id as
id1_12_0_, eventsajgj0_.id as id1_12_1_, eventsajgj0_.CODE as CODE2_12_1_,
eventsajgj0_.DATE as DATE3_12_1_, eventsajgj0_.FILE_ID as FILE_ID8_12_1_,
eventsajgj0_.MILLIS as MILLIS4_12_1_, eventsajgj0_.ORDER_NR as
ORDER_NR5_12_1_, eventsajgj0_.TYPE as TYPE6_12_1_, eventsajgj0_.VALUE as
VALUE7_12_1_ from eventdata eventsajgj0_ **where eventsajgj0_.FILE_ID=?**
order by eventsajgj0_.ORDER_NR
Please note the where clause in above query is against file_id(foreign key) and not id(eventdata primary key)
The dao structure is as follows
public class EventData implements Serializable {
private static final long serialVersionUID = 1L;
public EventData() {
}
#Id
#GeneratedValue(strategy=GenerationType.AUTO)
private int id;
#ManyToOne(fetch=FetchType.EAGER)
#JoinColumn(name="FILE_ID")
private ApplicationFile file;
getter & setters
}
public class ApplicationFile implements Serializable {
private static final long serialVersionUID = 1L;
#Id
#GeneratedValue(strategy=GenerationType.AUTO)
private int id;
bi-directional many-to-one association to Event
#OneToMany(mappedBy="file", cascade=CascadeType.ALL, fetch=FetchType.EAGER)
#OrderBy("orderNr")
private List<EventData> eventsajgjd;
getter & setters
}
my question is, why is it querying using file_id and not id when i'm executing a query on eventdata table.
PS:if i change fetch type of ApplicationFile as LAZY then the query executed is on id and not on file_id.
(added from Comment:)
CREATE TABLE eventdata (
ID int(11) NOT NULL AUTO_INCREMENT,
FILE_ID int(11) DEFAULT NULL,
PRIMARY KEY (ID),
KEY eventdata_ibfk_1 (FILE_ID),
CONSTRAINT eventdata_ibfk_1 FOREIGN KEY (FILE_ID)
REFERENCES files (ID) ON DELETE CASCADE
) ENGINE=InnoDB AUTO_INCREMENT=297502 DEFAULT CHARSET=utf8
I bet because you mapped the EventData / ApplicationFile bidirectionally (you have an attribute of type List<EventData> in ApplicationFile entity
So loading an EventData means eagerly loading the related ApplicationFile and so eagerly loading all related EventData.
I suppose that the related ApplicationFile instance is already in EntityManager L1 cache (otherwise the query should join on files table)
I have three tables
CREATE TABLE "ingredient" (
"id" INTEGER GENERATED BY DEFAULT AS IDENTITY(START WITH 1, INCREMENT BY 1) PRIMARY KEY,
"ingredient" VARCHAR(50) NOT NULL
);
CREATE TABLE "pizza" (
"id" INTEGER GENERATED BY DEFAULT AS IDENTITY(START WITH 1, INCREMENT BY 1) PRIMARY KEY,
"pizza" VARCHAR(50) NOT NULL
);
CREATE TABLE "pizza_structure" (
"pizza_id" INT NOT NULL,
"ingredient_id" INT NOT NULL,
"amount" INT NOT NULL
);
how to join them, to get Pizzas structure as a Map
#Entity
#Table(name = "ingredient")
public class Ingredient{
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
private String name;
public Ingredient() {
}
}
#Entity
#Table(name = "pizza")
public class Pizza {
#Id
#GeneratedValue
private Long id;
private String name;
#OneToMany ????
private Map<Ingredient, Integer> pizzaStructure;
public Pizza() {
}
public Pizza(String name, Map<Long, Integer> pizzaStructure) {
this.name = name;
this.pizzaStructure = pizzaStructure;
}
}
do I need to create #Embeddable class PizzaStructure, if yes when how to use it?
now I'm getting an error
Invocation of init method failed; nested exception is org.hibernate.AnnotationException: Use of #OneToMany or #ManyToMany targeting an unmapped class:
how to join them, to get Pizzas structure as a Map
It seems to look like this:
#ElementCollection
#CollectionTable(name = "pizza_structure", joinColumns = {#JoinColumn(name = "pizza_id")})
#Column(name = "amount")
#MapKeyJoinColumn(name = "ingredient_id")
private Map<Ingredient, Integer> pizzaStructure;
do I need to create #Embeddable class PizzaStructure
No.
More info is here: Hibernate User Guide - Maps.
Note that table pizza_structure should have foreign keys to pizza and ingredient tables and also unique constrain of pizza_id and ingredient_id, like this (it's postgresql dialect):
create table pizza_structure
(
pizza_id ... constraint fk_structure_pizza references pizza,
ingredient_id ... constraint fk_structure_ingredient references ingredient,
amount ...,
constraint pizza_structure_pkey primary key (pizza_id, ingredient_id)
);
You have a manyToMany relationship between pizza and ingredient and an additional column in your relationship.
I found a similar question here: JPA 2.0 many-to-many with extra column
(I would comment, but i do not have enough reputation.)
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.
used: hibernate 3.6.10, maven 2, postgres 9.
I have code that must work but it doesn't.
before I used hibernate 3.6.2 and got really frustrated error:
java.lang.ClassCastException: org.hibernate.action.DelayedPostInsertIdentifier cannot be cast to java.lang.Long
But after updating to 3.6.10 error became more sensible:
javax.persistence.PersistenceException: org.hibernate.PersistentObjectException: detached entity passed to persist
Code is a standart domain model:
Entity:
#Cache(usage = CacheConcurrencyStrategy.TRANSACTIONAL)
#Entity
#Table(schema = "simulators", name = "mySimulator_card")
public class MySimulatorCard {
#Id
#Column(name = "id")
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#Column(name = "account_number", unique = true, nullable = false)
private String accountNumber;
etc...
DAO:
public abstract class AbstractDao<E, PK extends Serializable> implements Dao<E, PK> {
private EntityManager entityManager;
public EntityManager getEntityManager() {
return entityManager;
}
#PersistenceContext(unitName = "MySimulator")
public void setEntityManager(EntityManager entityManager) {
this.entityManager = entityManager;
}
public abstract Class<E> getEntityClass();
#Override
public void persist(E e) {
getEntityManager().persist(e);//<-- some thing wroooong
}
etc...
And according table:
CREATE TABLE simulators.mySimulator_card
(
id bigserial NOT NULL,
account_number character varying(255) NOT NULL,
etc...
CONSTRAINT mySimulator_card_pk PRIMARY KEY (id),
CONSTRAINT mySimulator_card_account_fk FOREIGN KEY (account_id)
REFERENCES simulators.mySimulator_account (id) MATCH SIMPLE
ON UPDATE NO ACTION ON DELETE NO ACTION,
CONSTRAINT mySimulator_card_currency_fk FOREIGN KEY (currency_id)
REFERENCES simulators.mySimulator_currency ("name") MATCH SIMPLE
ON UPDATE NO ACTION ON DELETE NO ACTION,
CONSTRAINT mySimulator_card_product_fk FOREIGN KEY (product_id)
REFERENCES simulators.mySimulator_product (id) MATCH SIMPLE
ON UPDATE NO ACTION ON DELETE NO ACTION,
CONSTRAINT mySimulator_account_account_number_uq UNIQUE (account_number),
CONSTRAINT mySimulator_card_san_uq UNIQUE (san)
)
WITH (
OIDS=FALSE
);
ALTER TABLE simulators.mySimulator_card OWNER TO functional;
I suppose something wrong with context in functional tests because production is work fine. Also when I tried to use this trick to get session from entityManager:
EntityManager em = getEntityManager();
HibernateEntityManager hem = em.unwrap(HibernateEntityManager.class);
Session session = hem.getSession();
session.persist(e);
I've got the error:
java.lang.IllegalStateException: No transactional EntityManager available
Now sure that it is a good idea to post all test config because there are a lot of workaround stuff.
Any idea?
This error is raised among others when you try to persist a value in a column that is autogenerated.
Check if you're passing a value to persist for the column id, which you've marked as:
#GeneratedValue(strategy = GenerationType.IDENTITY)
Regards.