Disclaimer: This is my first Java project; learning as I go.
Background: I've inherited a legacy database on which to build a new RESTful API. We're using Elide with Spring Boot to provide a JSON API compliant service.
Reference: Example source code
Problem: We have entities with a many-to-many relationship to each other and themselves by way of a join table. Consider the followig schema:
CREATE TABLE ALPHA (
ID VARCHAR(255),
NAME VARCHAR(255),
CONSTRAINT PK_ALPHA PRIMARY KEY (ID)
);
CREATE TABLE BRAVO (
ID VARCHAR(255),
NAME VARCHAR(255),
CONSTRAINT PK_BRAVO PRIMARY KEY (ID)
);
CREATE TABLE RELATIONSHIP (
ID INT AUTO_INCREMENT,
FROM_ID VARCHAR(255),
TO_ID VARCHAR(255)
);
Where the resource entities are modeled as follows:
public class Alpha implements Serializable {
private String id;
private String name;
private Set<Alpha> alphas = new HashSet<>();
private Set<Bravo> bravos = new HashSet<>();
#Id
#Column(name = "ID", unique = true, nullable = false)
#GeneratedValue(generator = "uuid")
#GenericGenerator(name = "uuid", strategy = "uuid")
public String getId() {
return id;
}
public String getName() {
return name;
}
#ManyToMany(cascade = {CascadeType.PERSIST, CascadeType.MERGE})
#JoinTable(
name = "RELATIONSHIP",
joinColumns = #JoinColumn(name = "FROM_ID"),
inverseJoinColumns = #JoinColumn(name = "TO_ID")
)
public Set<Alpha> getAlphas() {
return alphas;
}
#ManyToMany(cascade = {CascadeType.PERSIST, CascadeType.MERGE})
#JoinTable(
name = "RELATIONSHIP",
joinColumns = #JoinColumn(name = "FROM_ID"),
inverseJoinColumns = #JoinColumn(name = "TO_ID")
)
public Set<Bravo> getBravos() {
return bravos;
}
}
And the relationship table:
public class Relationship implements Serializable {
private Integer id;
private String fromId;
private String toId;
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
public Integer getId() {
return id;
}
#Column(name = "FROM_ID")
public String getFromId() {
return fromId;
}
#Column(name = "TO_ID")
public String getToId() {
return toId;
}
}
Now let's say we have an Alpha record A1 with relationships to A2, A3, B1, and B2. First we delete the relationship to A2.
From our API this would be a DELETE request to http://localhost:9000/api/alphas/a1/relationships/alphas with BODY
{
"data": [
{
"type": "alphas",
"id": "a2"
}
]
}
Behind the scenes Hibernates does what I'm expecting and generates the following SQL queries:
2018-07-13 09:48:23.687 DEBUG 7964 --- [nio-9000-exec-5] org.hibernate.SQL :
Hibernate:
select
alpha0_.id as id1_0_,
alpha0_.name as name2_0_
from
alpha alpha0_
where
alpha0_.id in (
?
)
2018-07-13 09:48:23.688 TRACE 7964 --- [nio-9000-exec-5] o.h.type.descriptor.sql.BasicBinder : binding parameter [1] as [VARCHAR] - [a1]
2018-07-13 09:48:23.690 DEBUG 7964 --- [nio-9000-exec-5] org.hibernate.SQL :
Hibernate:
select
alphas0_.from_id as from_id2_2_0_,
alphas0_.to_id as to_id3_2_0_,
alpha1_.id as id1_0_1_,
alpha1_.name as name2_0_1_
from
relationship alphas0_
inner join
alpha alpha1_
on alphas0_.to_id=alpha1_.id
where
alphas0_.from_id=?
2018-07-13 09:48:23.690 TRACE 7964 --- [nio-9000-exec-5] o.h.type.descriptor.sql.BasicBinder : binding parameter [1] as [VARCHAR] - [a1]
2018-07-13 09:48:23.699 DEBUG 7964 --- [nio-9000-exec-5] org.hibernate.SQL :
Hibernate:
select
alpha0_.id as id1_0_,
alpha0_.name as name2_0_
from
alpha alpha0_
where
alpha0_.id in (
?
)
2018-07-13 09:48:23.699 TRACE 7964 --- [nio-9000-exec-5] o.h.type.descriptor.sql.BasicBinder : binding parameter [1] as [VARCHAR] - [a2]
2018-07-13 09:48:23.721 DEBUG 7964 --- [nio-9000-exec-5] org.hibernate.SQL :
Hibernate:
delete
from
relationship
where
from_id=?
and to_id=?
2018-07-13 09:48:23.722 TRACE 7964 --- [nio-9000-exec-5] o.h.type.descriptor.sql.BasicBinder : binding parameter [1] as [VARCHAR] - [a1]
2018-07-13 09:48:23.724 TRACE 7964 --- [nio-9000-exec-5] o.h.type.descriptor.sql.BasicBinder : binding parameter [2] as [VARCHAR] - [a2]
The key piece being delete from relationship where from_id=? and to_id=?
Now the problem arises when trying to delete the second Alpha relationship A3, in which Hibernate does almost the exact same sequence, except for the DELETE query which omits the and to_id=? from the query, i.e.
Hibernate:
delete
from
relationship
where
from_id=?
Which has the unintended consequence of deleting all other A1 relationships in the table, i.e. B1 and B2.
So that is the crux of my problem. It seems like Hibernate is only seeing one other related Alpha record and therefore deciding to simplify the query by omitting the and to_id statement.
I'm probably missing something terribly obvious!
I should also point out that I attempted to use a composite key on the relationship table but to no avail.
This is an unusual design, which I suspect is confusing Hibernate. Sharing a single join table between multiple Many-to-many relationships, isn't good database design, for one it can't have any foreign keys/referential integrity.
Secondly, Hibernate manages relationships, and therefore has control over the #JoinTable, I don't know how it would handle multiple entity relationships mapped with the same table. Evidently, not very well though!
The simplest solution (if you're able to), would be to have 2 mapping tables. One for the relationship between Alpha-Alpha and another between Alpha-Bravo.
Related
I'm sitting in this peculiar situation where I have the following database design
What this image illustrates is the following:
Table A has a_c_id which is "C id" but without a foreign key reference
Table B has c_id which is "C id" but without a foreign key reference.
Table C has id as primary key.
I'm trying in my java code to create following entities using hibernate and hibernate annotations
#Entity
#Table(name = "A")
Class A {
private Long id;
private List<B> bList; // This is the relation i'm struggling with
}
#Entity
#Table(name = "B")
Class B {
private Long Id;
private Long c_id;
}
I've tried alot of difference combinations using:
#JoinColumn(name = "a_c_id", referencedColumnName = "c_id")
private List<B> bList;
And:
#JoinTable(
name = "C",
joinColumns = {
#JoinColumn(
name = "id",
referencedColumnName = "a_c_id"
)
},
inverseJoinColumns = {
#JoinColumn(
name = "id",
referencedColumnName = "c_id"
),
}
)
private List<B> bList;
This last example with #JoinTable gives me an exception:
Repeated column in mapping for collection: id
which makes me wonder if mapping to the same PK from 2 different tables is not possible.
But i'm starting to run out of ideas on how to do this. I didn't make this database and is wondering if it's even possible to achieve making a relation from A to B without a foreign key to reference.
I thought I could perhaps use #JoinTable, however they refer to the same ID rather than 2 different ones as you would in a JoinTable.
If anyone has some input on what to read up on, or perhaps could come with an idea on how to achieve this without changing the database design, I'd apppreciate it!
EDIT: I've added images of table records:
EDIT 2: I've tried to trace binding values based on me and Nikos conversation:
Hibernate: select a0_.id as id1_0_0_, a0_.a_c_id as a_c_id2_0_0_ from a a0_ where a0_.id=?
2022-07-27 10:25:06.101 TRACE 16404 --- [nio-8080-exec-8] o.h.type.descriptor.sql.BasicBinder : binding parameter [1] as [BIGINT] - [1]
2022-07-27 10:25:06.107 TRACE 16404 --- [nio-8080-exec-8] o.h.type.descriptor.sql.BasicExtractor : extracted value ([a_c_id2_0_0_] : [BIGINT]) - [41]
2022-07-27 10:25:06.113 TRACE 16404 --- [nio-8080-exec-8] org.hibernate.type.CollectionType : Created collection wrapper: [eu.sos.auditing.models.HibernateTestModels.A.bList#1]
Hibernate: select blist0_.c_id as c_id2_3_0_, blist0_.id as id1_3_0_, blist0_.id as id1_3_1_, blist0_.c_id as c_id2_3_1_ from b blist0_ where blist0_.c_id=?
2022-07-27 10:25:06.149 TRACE 16404 --- [nio-8080-exec-8] o.h.type.descriptor.sql.BasicBinder : binding parameter [1] as [BIGINT] - [1]
EDIT 3: What solved my issue.
Due to Nikos help I was able to solve my issue by changing my class A to the following:
#Entity
#Table(name = "A")
#Data
public class A implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#Column(name = "a_c_id")
private Long aCId;
#OneToMany
#JoinColumn(name="c_id", referencedColumnName = "a_c_id")
private List<B> bList;
}
Kind regards
The solution can be as straightforward as pretending that B.c_id points to A.a_c_id:
Class A {
#OneToMany
// remember, the JoinColumn is in the other table (here B)
// while the referencedColumnName is in this table
#JoinColumn(name="c_id ", referencedColumnName = "a_c_id")
private List<B> bList;
}
EDIT: It is important to include the referencedColumnName, which I has forgotten before the edit. Also see full solution below for another caveat with Serializable.
If the DB constraints are different, you will have to make sure a C record exists before inserting into A and B.
The other solution is NOT to map the relation in the entity classes. Or, rather, map the existing relations, i.e. any of: (1) "A relates to 1 C", (2) "B relates to 1 C", (3) "C relates to many As", (4) "C relates to many Bs". And fetch the Bs that correspond to an A with an independent query.
FULL WORKING SOLUTION (with slightly different names - omitting getters/setters for brevity)
The entities:
#Entity
#Table(name = "AA")
public class Alpha implements Serializable {
#Id
private Long id;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "a_c_id")
private Charlie charlie;
#OneToMany
#JoinColumn(name="c_id", referencedColumnName = "a_c_id")
private List<Bravo> bList;
}
CAVEAT: I had to make Alpha implements Serializable because of HHH-7668. It applies only to the Hibernate implementation of JPA.
#Entity
#Table(name = "BB")
public class Bravo {
#Id
private Long id;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "c_id")
private Charlie charlie;
}
#Entity
#Table(name = "CC")
public class Charlie {
#Id
private Long id;
}
Sample code to read, assuming the contents of the tables are as in the question:
EntityManager em = ...;
Alpha a = em.find(Alpha.class, 2L);
a.getbList().forEach(b -> System.out.println(b.getId()));
// prints 3, 4
One of my entities is throwing a StaleStateException when trying to delete it. Code in question:
#RestController
#RequestMapping("/cycles")
public class DeleteCycleController {
#DeleteMapping("{cycleId}")
#ResponseStatus(HttpStatus.NO_CONTENT)
public void deleteCycleById(#PathVariable("cycleId") UUID cycleId, Principal principal) {
// cycleRepository extends CrudRepository
cycleRepository.deleteById(new CycleId(cycleId));
// deleter.deleteCycle(new CycleId(cycleId));
}
}
Throws the following:
org.springframework.orm.ObjectOptimisticLockingFailureException: Batch update returned unexpected row count from update [0]; actual row count: 0; expected: 1; statement executed: HikariProxyPreparedStatement#716092925 wrapping delete from cycle where id='c4c1428e-c296-4199-85f6-8ef16e6999c9'::uuid; nested exception is org.hibernate.StaleStateException: Batch update returned unexpected row count from update [0]; actual row count: 0; expected: 1; statement executed: HikariProxyPreparedStatement#716092925 wrapping delete from cycle where id='c4c1428e-c296-4199-85f6-8ef16e6999c9'::uuid
Here are the hibernate logs just before the failure:
2021-03-11 20:34:11.532 DEBUG 55483 --- [nio-8080-exec-7] org.hibernate.SQL : requestId=b53dd96089f748cf942fc46867d32fb5, requestMethod=DELETE, correlationId=b53dd96089f748cf942fc46867d32fb5, requestURI=/cycles/c4c1428e-c296-4199-85f6-8ef16e6999c9, userId=5ae11e82-f8df-4fad-ae10-1aa57423ba31, orgId=c7389774-9afa-452b-aa1c-74e7b18bc04d : select cycle0_.id as id1_0_0_, cycle0_.attributes as attribut2_0_0_, cycle0_.created_at as created_3_0_0_, cycle0_.cycle_type_id as cycle_ty4_0_0_, cycle0_.description as descript5_0_0_, cycle0_.end_time as end_time6_0_0_, cycle0_.start_time as start_ti7_0_0_, cycle0_.end_location as end_loca8_0_0_, cycle0_.last_modified_at as last_mod9_0_0_, cycle0_.name as name10_0_0_, cycle0_.organization_id as organiz11_0_0_, cycle0_.start_location as start_l12_0_0_ from cycle cycle0_ where cycle0_.id=?
Hibernate: select cycle0_.id as id1_0_0_, cycle0_.attributes as attribut2_0_0_, cycle0_.created_at as created_3_0_0_, cycle0_.cycle_type_id as cycle_ty4_0_0_, cycle0_.description as descript5_0_0_, cycle0_.end_time as end_time6_0_0_, cycle0_.start_time as start_ti7_0_0_, cycle0_.end_location as end_loca8_0_0_, cycle0_.last_modified_at as last_mod9_0_0_, cycle0_.name as name10_0_0_, cycle0_.organization_id as organiz11_0_0_, cycle0_.start_location as start_l12_0_0_ from cycle cycle0_ where cycle0_.id=?
2021-03-11 20:34:11.533 TRACE 55483 --- [nio-8080-exec-7] o.h.type.descriptor.sql.BasicBinder : requestId=b53dd96089f748cf942fc46867d32fb5, requestMethod=DELETE, correlationId=b53dd96089f748cf942fc46867d32fb5, requestURI=/cycles/c4c1428e-c296-4199-85f6-8ef16e6999c9, userId=5ae11e82-f8df-4fad-ae10-1aa57423ba31, orgId=c7389774-9afa-452b-aa1c-74e7b18bc04d : binding parameter [1] as [OTHER] - [c4c1428e-c296-4199-85f6-8ef16e6999c9]
2021-03-11 20:34:11.542 DEBUG 55483 --- [nio-8080-exec-7] org.hibernate.SQL : requestId=b53dd96089f748cf942fc46867d32fb5, requestMethod=DELETE, correlationId=b53dd96089f748cf942fc46867d32fb5, requestURI=/cycles/c4c1428e-c296-4199-85f6-8ef16e6999c9, userId=5ae11e82-f8df-4fad-ae10-1aa57423ba31, orgId=c7389774-9afa-452b-aa1c-74e7b18bc04d : delete from cycle where id=?
Hibernate: delete from cycle where id=?
2021-03-11 20:34:11.542 TRACE 55483 --- [nio-8080-exec-7] o.h.type.descriptor.sql.BasicBinder : requestId=b53dd96089f748cf942fc46867d32fb5, requestMethod=DELETE, correlationId=b53dd96089f748cf942fc46867d32fb5, requestURI=/cycles/c4c1428e-c296-4199-85f6-8ef16e6999c9, userId=5ae11e82-f8df-4fad-ae10-1aa57423ba31, orgId=c7389774-9afa-452b-aa1c-74e7b18bc04d : binding parameter [1] as [OTHER] - [c4c1428e-c296-4199-85f6-8ef16e6999c9]
And the Cycle entity:
#javax.persistence.Entity
#Accessors(fluent = true)
#Getter
#NoArgsConstructor(access = AccessLevel.PRIVATE)
#Builder
#TypeDefs({#TypeDef(name = "jsonb", typeClass = JsonBinaryType.class)})
public class Cycle {
#Id private UUID id;
private CycleName name;
private CycleDescription description;
private UUID cycleTypeId;
private UUID organizationId;
private Instant createdAt;
private Instant lastModifiedAt;
#Type(type = "jsonb")
private Map<String, Object> attributes;
private CycleDuration duration;
#Column(columnDefinition = "Geometry", nullable = true)
private Geometry startLocation;
private Geometry endLocation;
#OneToMany(mappedBy = "cycleId", cascade = CascadeType.ALL, orphanRemoval = false)
#Getter(AccessLevel.PRIVATE)
#Setter(AccessLevel.PUBLIC)
private List<CycleEntityMapping> entities = new ArrayList<>();
...
}
Any ideas as to what could be going wrong? Another (simpler) entity deletes fine.
Not sure if its the OneToMany causing issues, but I've tried changing the cascade type, orphan removal and fetch type with no luck. Otherwise, could it be caused by the value objects? Note that updating the entity works fine - it's only failing on delete.
In this case, it was caused by a bug in a before delete trigger. There was a bug in the trigger causing the row to not be deleted:
CREATE OR REPLACE FUNCTION trigger_delete_row_backup()
RETURNS trigger AS
$BODY$
BEGIN
INSERT INTO deleted_cycle values (OLD.*);
RETURN NEW;
-- should actually be RETURN OLD
END;
$BODY$
language PLPGSQL;
Once the trigger was fixed, the issue no was resolved.
I am new using Spring Boot and Hibernate and I am working on a project that has a simple Many to Many association. A sport Center has many services and viceversa. When i try to enter a json like this to create a new sport center and associate some existing service:
{
"name" : "SC1",
"address" : "Street 1111",
"description" : "A Sport center",
"email" : "sc#sc.com",
"phones" : [{"number" : "123456"}, {"number" : "654321"}],
"services" : [{"idService" : 1}, {"idService" : 2}],
"commune" : {"idCommune" : 1},
"users" : [{"idUser":62}]
}
I am receiving this error:
2018-06-25 03:56:32.670 DEBUG 20696 --- [nio-8080-exec-6] org.hibernate.SQL : insert into sport_center (address, Commune_idCommune, description, email, name) values (?, ?, ?, ?, ?)
2018-06-25 03:56:32.671 TRACE 20696 --- [nio-8080-exec-6] o.h.type.descriptor.sql.BasicBinder : binding parameter [1] as [VARCHAR] - [Street 1111]
2018-06-25 03:56:32.671 TRACE 20696 --- [nio-8080-exec-6] o.h.type.descriptor.sql.BasicBinder : binding parameter [2] as [INTEGER] - [1]
2018-06-25 03:56:32.671 TRACE 20696 --- [nio-8080-exec-6] o.h.type.descriptor.sql.BasicBinder : binding parameter [3] as [VARCHAR] - [A Sport center]
2018-06-25 03:56:32.671 TRACE 20696 --- [nio-8080-exec-6] o.h.type.descriptor.sql.BasicBinder : binding parameter [4] as [VARCHAR] - [sc#sc.com]
2018-06-25 03:56:32.671 TRACE 20696 --- [nio-8080-exec-6] o.h.type.descriptor.sql.BasicBinder : binding parameter [5] as [VARCHAR] - [SC1]
2018-06-25 03:56:32.800 DEBUG 20696 --- [nio-8080-exec-6] org.hibernate.SQL : insert into phone (number, Sport_center_idSport_Center) values (?, ?)
2018-06-25 03:56:32.800 TRACE 20696 --- [nio-8080-exec-6] o.h.type.descriptor.sql.BasicBinder : binding parameter [1] as [BIGINT] - [123456]
2018-06-25 03:56:32.800 TRACE 20696 --- [nio-8080-exec-6] o.h.type.descriptor.sql.BasicBinder : binding parameter [2] as [INTEGER] - [252]
2018-06-25 03:56:32.942 DEBUG 20696 --- [nio-8080-exec-6] org.hibernate.SQL : insert into phone (number, Sport_center_idSport_Center) values (?, ?)
2018-06-25 03:56:32.942 TRACE 20696 --- [nio-8080-exec-6] o.h.type.descriptor.sql.BasicBinder : binding parameter [1] as [BIGINT] - [654321]
2018-06-25 03:56:32.942 TRACE 20696 --- [nio-8080-exec-6] o.h.type.descriptor.sql.BasicBinder : binding parameter [2] as [INTEGER] - [252]
2018-06-25 03:56:33.368 ERROR 20696 --- [nio-8080-exec-6] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is org.springframework.dao.InvalidDataAccessApiUsageException: detached entity passed to persist: com.eiffel.canchai.model.Service; nested exception is org.hibernate.PersistentObjectException: detached entity passed to persist: com.eiffel.canchai.model.Service] with root cause
org.hibernate.PersistentObjectException: detached entity passed to persist: com.eiffel.canchai.model.Service
It is important to mention that I have already added some rows in the service table. So, the idea is to add a Sport Center and associate these services to it.
Could you please help me with this?
My classes below:
SportCenter.java
#Entity
#Table(name = "sport_center")
public class SportCenter implements Serializable{
private static final long serialVersionUID = 1L;
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "idSport_Center")
private Integer idSportCenter;
#Column(name = "name")
private String name;
#Column(name = "address")
private String address;
#Column(name = "description")
private String description;
#Column(name = "email")
private String email;
#JoinTable(name = "sport_center_has_user", joinColumns = {
#JoinColumn(name = "Sport_center_idSport_Center", referencedColumnName = "idSport_Center")}, inverseJoinColumns = {
#JoinColumn(name = "User_idUser", referencedColumnName = "idUser")})
#ManyToMany(cascade = CascadeType.ALL)
private List<User> users;
#JoinTable(name = "sport_center_has_service", joinColumns = {
#JoinColumn(name = "Sport_center_idSport_Center", referencedColumnName = "idSport_Center")}, inverseJoinColumns = {
#JoinColumn(name = "Service_idService", referencedColumnName = "idService")})
#ManyToMany(cascade = CascadeType.ALL)
private List<Service> services;
#OneToMany(cascade = CascadeType.ALL, mappedBy = "sportCenter")
#JsonIgnore
private List<ImageField> imageFields;
#OneToMany(cascade = CascadeType.ALL, mappedBy = "sportCenter")
#JsonIgnore
private List<Field> fields;
#OneToMany(cascade = CascadeType.ALL, mappedBy = "sportCenter", fetch = FetchType.LAZY)
private List<Phone> phones;
#JoinColumn(name = "Commune_idCommune", referencedColumnName = "idCommune")
#ManyToOne(optional = false)
private Commune commune;
public SportCenter() {
}
Service.java
#Entity
#Table(name = "service")
public class Service implements Serializable {
private static final long serialVersionUID = 1L;
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "idService")
private Integer idService;
#Column(name = "name")
private String name;
#ManyToMany(fetch = FetchType.LAZY, mappedBy = "services", cascade = CascadeType.ALL)
#JsonIgnore
private List<SportCenter> sportCenters;
public Service() {
}
ServiceDao.java
#Repository
#Transactional
public class ServiceDao implements IServiceDao {
#PersistenceContext
private EntityManager entityManager;
#Override
public void save(Service entity) {
entityManager.persist(entity);
}
The SportCenterDao is almost the same that the above :)
Controller
#Controller
#RequestMapping("/sportcenter")
#CrossOrigin
public class SportCenterController {
#Autowired
private ISportCenterService sportCenterService;
#PostMapping("/register")
public ResponseEntity<?> registerSportCenter(#RequestBody SportCenter sc){
if (sc.getEmail().length() == 0 || sc.getName().length() == 0 || sc.getAddress().length() == 0 || sc.getPhones().size() == 0 || sc.getServices().size() == 0) {
return new ResponseEntity(new ErrorMsg("Check parameters. One of them is missing"),HttpStatus.NO_CONTENT);
}
for (Phone p : sc.getPhones()) {
p.setSportCenter(sc);
}
for (Service s : sc.getServices()) {
s.addSportCenters(sc);
}
sportCenterService.save(sc);
return new ResponseEntity(HttpStatus.OK);
}
I think that probably is an error mapping the associations but I am not sure.
Also, if you see that there is a way to improve my code, please let me know :).. Advices are always good.
I am using Spring Boot V2.0.3 and Hibernate 5.3
Thanks in advance!
The problem here is that you are cascading the persist event from parent to child.
When the child entity has a non-zero ID, then hibernate will assume it already exists.
Since you are getting the data from the rest service, I guess the child "Service" entities are not detached. Hence the error "Detached entity passed to persist".
You need to persist the parent first, make the children managed via merge call, then link the parent to the child entities, or get rid of the Cascade.All.
I have the following JPA Mapping (getters and setters out for brevity purposes, the DDL also gets generated from the code which may/may not play a role):
Expense
#Entity
public class Expense {
#Id
#GeneratedValue
private Long id;
private String name;
private Long amount;
private Boolean monthly;
#OneToOne
#JoinColumn(name = "category")
#Fetch(FetchMode.JOIN)
private Category category;
#ManyToMany(cascade = {CascadeType.PERSIST, CascadeType.MERGE})
#JsonInclude(JsonInclude.Include.NON_EMPTY)
private List<Label> labels = new ArrayList<>();
//constructor, getters and setters...
}
Category
#Entity
public class Category {
#Id
private String name;
//constructor, getters and setters...
}
Label
#Entity
public class Label {
#Id
private String name;
//constructor, getters and setters...
}
Usage with JpaRepository
So I am using a JpaRepository that looks like this:
public interface ExpensesRepository extends JpaRepository<Expense, Long> {
#Query("SELECT e FROM Expense e LEFT JOIN FETCH e.category")
List<Expense> findAllExpensesExploded();
}
When I use the default findAll() method of the JpaRepository, I get a n+1 select problem:
2017-01-03 19:35:22.665 DEBUG 26040 --- [nio-8080-exec-1] org.hibernate.SQL : select expense0_.id as id1_1_, expense0_.amount as amount2_1_, expense0_.category_name as category5_1_, expense0_.monthly as monthly3_1_, expense0_.name as name4_1_ from expense expense0_
2017-01-03 19:35:22.673 DEBUG 26040 --- [nio-8080-exec-1] org.hibernate.SQL : select category0_.name as name1_0_0_ from category category0_ where category0_.name=?
2017-01-03 19:35:22.674 TRACE 26040 --- [nio-8080-exec-1] o.h.type.descriptor.sql.BasicBinder : binding parameter [1] as [VARCHAR] - [Rent]
2017-01-03 19:35:22.682 DEBUG 26040 --- [nio-8080-exec-1] org.hibernate.SQL : select category0_.name as name1_0_0_ from category category0_ where category0_.name=?
2017-01-03 19:35:22.683 TRACE 26040 --- [nio-8080-exec-1] o.h.type.descriptor.sql.BasicBinder : binding parameter [1] as [VARCHAR] - [Insurance]
However, when I use my own findAllExpensesExploded() method I get a single SQL query:
2017-01-03 19:35:22.691 DEBUG 26040 --- [nio-8080-exec-1] org.hibernate.SQL : select expense0_.id as id1_1_0_, category1_.name as name1_0_1_, expense0_.amount as amount2_1_0_, expense0_.category_name as category5_1_0_, expense0_.monthly as monthly3_1_0_, expense0_.name as name4_1_0_ from expense expense0_ left outer join category category1_ on expense0_.category_name=category1_.name
My expectation was for both findAll() and findAllExpensesExploded() to be executed with a single SQL query.
My query worked because I seemed to have constructed it correctly
But why does the findAll() not work with the given mapping annotations? Is it possible that Spring Data is ignoring the #Fetch annotation?
An additional question that seems reasonable to ask is whether the default findAll() should only be used for simple entities? (where simple is defined as no associations).
The Default fetch mode is lazy. It's always a good practice to use #NamedEntityGraph and #EntityGraph annotations when working with Spring Data JPA.You can go through this
whether the default findAll() should only be used for simple entities? (where simple is defined as no associations).
Fetch Mode - LAZY will only fire for primary table. If in the code you call any other method that has a parent table dependency then it will fire Fetch Mode - SELECT.
In a spring mvc application using hibernate and MySQL, I am getting the following constraint violation exception:
Caused by: com.mysql.jdbc.exceptions.jdbc4.MySQLIntegrityConstraintViolationException:
Cannot add or update a child row: a foreign key constraint fails
(`mybd`.`hl7_documententity`, CONSTRAINT `hl7_documententity_ibfk_1`
FOREIGN KEY (`ptcode`, `ptcodesystem`)
REFERENCES `hl7_generalcode` (`code`, `codesystem`))
The problem occurs when I try to save a DocumentEntity containing a property of type GeneralCode, both of which are defined below.
I have read many postings and blogs on this error, but none seem to resolve my problem. How can I resolve this error?
Here is the DocumentEntity class:
#Entity
#Table(name = "hl7_documententity")
public class HL7DocumentEntity extends BaseEntity{
//other properties
#ManyToOne
#JoinColumns({ #JoinColumn(name = "ptcode", referencedColumnName = "code"),
#JoinColumn(name = "ptcodesystem", referencedColumnName = "codesystem")
})
private HL7GeneralCode providertype;
//getters and setters
}
Here is the GeneralCode class:
#Entity
#Table(name = "hl7_generalcodes")
public class HL7GeneralCode implements Serializable{
private static final long serialVersionUID = -8620565054475096516L;
#EmbeddedId
private HL7EmbedCodePK codePk;
#OneToMany(mappedBy = "providertype")
private Set<HL7DocumentEntity> documententities;
////////////getters and setters
}
Here is the code from the controller:
HL7GeneralCode authcode = processGeneralCode(grandkid);
HL7GeneralCode testcode = this.clinicService.findGeneralCodeByPK(authcode.getCodePk().getCode(), authcode.getCodePk().getCodesystem());
if(testcode==null){
authcode.addDocumententity(mydent);
this.clinicService.savehl7GeneralCode(authcode);
mydent.setProvidertype(authcode);
//this next line throws the error
this.clinicService.savehl7DocumentEntity(mydent);
}else{
//other stuff
}
Here is the dao method:
#Repository
public class JpaSomethingRepositoryImpl implements SomethingRepository {
#PersistenceContext
private EntityManager em;
#Override
#Transactional
public void savehl7DocumentEntity(HL7DocumentEntity de) {
HL7GeneralCode code = de.getProvidertype();
if(code !=null && code.getCodePk()==null){//HL7GeneralCode is not persistent. We don't support that
throw new IllegalStateException("Cannot persist an adress using a non persistent HL7GeneralCode");
}
System.out.println("=========================== inside jpaCdaRespository.saveDocEntity(de)");
de.setProvidertype(null);
if(code.getDocumententities()!=null){
ArrayList<HL7DocumentEntity> addrList = new ArrayList<HL7DocumentEntity>();
addrList.addAll(code.getDocumententities());
addrList.remove(de);
Set<HL7DocumentEntity> myaddrs = new HashSet<HL7DocumentEntity>(addrList);
code.setDocumententities(myaddrs);
}
code = em.merge(code);
de.setProvidertype(code);
code.addDocumententity(de);
if (de.getId() == null) {
System.out.println("[[[[[[[[[[[[ about to persist de ]]]]]]]]]]]]]]]]]]]]");
em.persist(de);
} else {
System.out.println("]]]]]]]]]]]]]]]]]] about to merge de [[[[[[[[[[[[[[[[[[[[[");
de = em.merge(de);
}
}
}
The executed SQL statement and the actual values that hibernate is trying to insert via the sql are:
[[[[[[[[[[[[ about to persist de ]]]]]]]]]]]]]]]]]]]]
DEBUG SQL - insert into hl7_documententity (author_id, authpar_id, entitytype, id_extension, id_root, ptcode, ptcodesystem, id) values (?, ?, ?, ?, ?, ?, ?, ?)
Hibernate: insert into hl7_documententity (author_id, authpar_id, entitytype, id_extension, id_root, ptcode, ptcodesystem, id) values (?, ?, ?, ?, ?, ?, ?, ?)
TRACE BasicBinder - binding parameter [1] as [INTEGER] - <null>
TRACE BasicBinder - binding parameter [2] as [INTEGER] - <null>
TRACE BasicBinder - binding parameter [3] as [VARCHAR] - <null>
TRACE BasicBinder - binding parameter [4] as [VARCHAR] - NI
TRACE BasicBinder - binding parameter [5] as [VARCHAR] - nullFlavor
TRACE BasicBinder - binding parameter [6] as [VARCHAR] - UNK
TRACE BasicBinder - binding parameter [7] as [VARCHAR] - HL7NullFlavor
TRACE BasicBinder - binding parameter [8] as [INTEGER] - 32787
WARN SqlExceptionHelper - SQL Error: 1452, SQLState: 23000
ERROR SqlExceptionHelper - Cannot add or update a child row: a foreign key constraint fails (`docbd`.`hl7_documententity`, CONSTRAINT `hl7_documententity_ibfk_1` FOREIGN KEY (`ptcode`, `ptcodesystem`) REFERENCES `hl7_generalcode` (`code`, `codesystem`))
INFO AbstractBatchImpl - HHH000010: On release of batch it still contained JDBC statements
WARN warn - Handler execution resulted in exception
You can read the EmbedCodePK class code by clicking on this link.
You can read the entire stack trace by clicking on this link.
Here is a link to the code for the BaseEntity class.
Change this:
#OneToMany(mappedBy = "providertype")
private Set<HL7DocumentEntity> documententities;
To this:
#OneToMany(fetch = FetchType.LAZY)
#JoinTable(name = "Link_Documents", joinColumns = {#JoinColumn(name = "codePk", unique = true)}, inverseJoinColumns = {#JoinColumn(name = "change_this_with_primary_key_variable_name_from_HL7DocumentEntity")})
private Set<HL7DocumentEntity> documententities;
And in HL7DocumentEntity change as follows:
This
#ManyToOne
#JoinColumns({ #JoinColumn(name = "ptcode", referencedColumnName = "code"),
#JoinColumn(name = "ptcodesystem", referencedColumnName = "codesystem")
})
private HL7GeneralCode providertype;
Change to this:
#ManyToOne(fetch = FetchType.LAZY)
#JoinTable(name = "Link_Documents", joinColumns = {#JoinColumn(name = "change_this_with_primary_key_variable_name_from_HL7DocumentEntity")}, inverseJoinColumns = {#JoinColumn(name = "codePk")})
private HL7GeneralCode providertype;
I think you have to change "change_this_with_primary_key_variable_name_from_HL7DocumentEntity" with "id" like it is in BaseEntity but take a look at your sql table, you willsee there the correct name.
I hope you notice How I told JPA to use the same "Link_Documents" table for linking the 2 tables. I think this is were your mistake is. Just make sure to change where I told you with the correct variable name and I think it should work
you know i would start by cleaning up this code.
Though the hibernate documentation might tell you placing those annotations on the field variables is ok,,, its a bad idea as a standard. field level requires hibernate to create a proxy before the field can be access in the case of lazy fetches. so just get use to doing it correctly and you wont have the misfortune of finding issues. move all those annotations to the getters as property level access doesnt require the proxy.
field level variables should be private.
you are casting an Integer to an Integer in there... some of that stuff makes me not want to look at it and truthfully will probably be a silly bit of laziness that ends up being your problem.
while you are at it go ahead an initialize the set with new HashSet(0); Things like that. Still broken update us.