Hibernate ManyToMany: doesn't insert the same ID in both tables - java

I have a (classic) many to many relationship using Hibernate and a Oracle database.
I defined my entities as follows.
Student.java
#Entity
#Table
public class Student implements Serializable {
#Id
#GeneratedValue(strategy=GenerationType.AUTO)
#Column(name = "STUDENT_ID")
private Long studentId;
#ManyToMany
#JoinTable(name = "STUDENT_PROJECT", joinColumns = {
#JoinColumn(name = "STUDENT_ID") }, inverseJoinColumns = {
#JoinColumn(name = "PROJECT_ID") })
private Set<Project> projects = new HashSet<>();
Project.java
#Entity
#Table
public class Project implements Serializable {
#Id
#Column(name="PROJECT_ID")
private int projectId;
#ManyToMany(mappedBy="projects")
private Set<Student> students = new HashSet<>();
I have a STUDENT_PROJECT table in my oracle database that consists of two fields, PROJECT_ID and STUDENT_ID with a composite primary key on them.
I have a sequence on my STUDENT table to auto generate their ID.
TRIGGER STUDENT_TRIGGER
BEFORE INSERT ON STUDENT
FOR EACH ROW
BEGIN
SELECT STUDENT_SEQUENCE.NEXTVAL
INTO :STUDENT_ID
FROM dual;
END;
Now my problem is that when I try to persist my Student entity, the ID field of the STUDENT table doesn't correspond to the STUDENT_ID field of STUDENT_PROJECT table.
Somehow it persists an ID for Student and a different one in the mapping table and I can't figure out why.
This is how I manipulate my objects
Student student = new Student();
// set some fields
Set<Project> projects = new HashSet<>();
// call to a private method to set its projects
student.setProjects(projects);
studentDao.persist(student);
I had to remove the foreign key in the STUDENT_PROJECT table on the ID of STUDENT (else the constraint wouldn't let me save of course) to finally notice that is was setting differents ID's but I don't understand why.
If you need more information let me know, I tried to keep it as small as possible, thanks.
UPDATE:
I have tried to remove the trigger on the STUDENT table and changed its Java configuration to
#Id
#GeneratedValue(strategy = GenerationType.AUTO, generator = "G1")
#SequenceGenerator(name = "G1", sequenceName= "STUDENT_SEQUENCE")
#Column(name = "STUDENT_ID")
private Long studentId;
I now get:
javax.persistence.EntityExistsException: A different object with the same identifier value was already associated with the session : [fr.persistance.data.Student#3037]
It doesn't save to database at all and produces the above exception on the persist(), I guess I have a problem with my sequence as it seems it tries to persist two Student objects with the same ID (I am looping to save multiples students and their projects).
I have used the eclipse debug to inspect a few of my Student objects after the persist() call and they each have a different studentId in Java but once the loop and the transaction end, the exception occurs but it does seem like they each get a separate ID.

The problem is that the Ids of the students are generated by the database in a trigger, while Hibernate tries to create the value itself.
For an oracle database, GenerationType.AUTO will use a sequence. The sequence is accessed by Hibernate, which assigns the value (before the student is actually inserted in the database).
To make Hibernate use your sequence, add
#SequenceGenerator(name = "sequence-generator", sequenceName = "STUDENT_SEQUENCE")
and remove the trigger.
When you want to use a trigger, you need to map it as GenerationType.IDENTITY. (Not recommended though.)

Related

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: Entity referenced by a view as a #ManyToOne association

Currently, my database is organized in a way that I have the following relationships(in a simplified manner):
#Entity
class A {
/*... class A columns */
#Id #NotNull
private Long id;
}
#Entity
#Immutable
#Table(name = "b_view")
class B {
/* ... same columns as class A, but no setters */
#Id #NotNull
private Long id;
}
The B entity is actually defined by a VIEW, which is written in this manner(assuming Postgres):
CREATE VIEW b_view AS
SELECT a.* FROM a WHERE EXISTS
(SELECT 1 FROM filter_table ft WHERE a.id = ft.b_id);
The idea here is that B references all elements of A that are present on filter_table. filter_table is another view that isn't really important, but it's the result of joining the A table with another, unrelated table, through a non-trivial comparison of substrings. These views are done so that I don't need to duplicate and control which elements of A also show up in B.
All of these are completely fine. JpaRepository is working great for B(obviously without saving the data, as B is Immutable) and it's all good.
However, at one point we have an entity that has a relationship with B objects:
#Entity
class SortOfRelatedEntity {
/** ... other columns of SortOfRelatedEntity */
#ManyToOne(fetch = FetchType.EAGER, targetEntity = Fornecedor.class)
#JoinColumn(name = "b_id", foreignKey = #ForeignKey(foreignKeyDefinition = "references a(id)"))
private B b;
}
For obvious reasons, I can't make this foreign key reference "b", since B is a view. However, I do want the query for searching this attribute to be defined by the b_view table, and having the foreign key defined by the underlying table(as written above) would be also nice in order to guarantee DB integrity.
However, when applying the above snippet, my sort-of-related-entity table doesn't create a foreign key as I would have expected. For the record, I'm using Hibernate 5.2.16 atm.
What am I doing wrong? Is this even possible? Is there something else I should do that I'm not aware of?
Oh FFS
I realized my mistake now. This:
#JoinColumn(name = "b_id", foreignKey = #ForeignKey(foreignKeyDefinition = "references a(id)"))
Should have been this:
#JoinColumn(name = "b_id", foreignKey = #ForeignKey(foreignKeyDefinition = "foreign key(b_id) references a(id)"))
Notice that the foreignKeyDefinition must include foreign key(), not just the references part.
Hopefully this helps someone in the future.

Mapping multiple tables to one List Hibernate

I've been searching over the web to find out a solution for this. It seems nobody has the answer... I start thinking i'm in wrong way adressing the problem.
Let's see if i can explain easy.
Im developing a contract maintenance. (table: contrat_mercan). For the contract, we will select a category (table: categoria), each category has qualities (table: calidad) in relation 1 - N (relationship table categoria_calidad).
This qualities must have a value for each contract where the category is selected, so I created a table to cover this relationship: contrato_categoria_calidad.
#Entity
#Table(name = "contrato_categoria_calidad")
public class ContratoCategoriaCalidad implements Serializable{
// Constants --------------------------------------------------------
private static final long serialVersionUID = -1821053251702048097L;
// Fields -----------------------------------------------------------
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#Column(name = "CCC_ID")
private int id;
#Column(name = "CONTRAT_MERCAN_ID")
private int contratoId;
#Column(name = "CATEGORIA_ID")
private int categoriaId;
#Column(name = "CALIDAD_ID")
private int calidadId;
#Column(name = "VALOR")
private double valor;
.... getters/ setters
In this table I wanted to avoid having an Id, three fields are marked as FK in database and first attempts where with #JoinColumn in the three fields. But it does not worked for hibernate.
Anyway, now ContratoCategoriaCalidad is behaving okay as independent entity. But I will need to implement all maintenance, updates, deletes for each case manually... :(
What I really want, (and I think is a better practice) is a cascade when I saveOrUpdate the contract as the other entities do, but I don't find the way to make a List in contrat_mercan table.
This is working perfect for other relationships in same table:
#OneToOne
#JoinColumn(name="CONDICION")
private Condicion condicion;
#OneToMany (cascade = {CascadeType.ALL})
#JoinTable(
name="contrato_mercan_condicion",
joinColumns = #JoinColumn( name="CONTRATO_MERCAN_ID")
,inverseJoinColumns = #JoinColumn( name="CONDICION_ID")
)
private List<Condicion> condiciones;
But all my attempts to map this failed, what i want, is to have in my Java entity contrat_mercan a field like this:
private List<ContratoCategoriaCalidad> relacionContratoCategoriaCalidad;
not a real column in database, just representation of the relationship.
I found solutions to join multiple fields of the same table, here, and here, but not to make a relationship with 3 tables...
Any idea? Im doing something wrong? Maybe i must use intermediate table categoria_calidad to perform this?
Thanks!!
If you want to access a list of related ContratoCategoriaCalidad objects from Contrato entity you need to declare a relationship between those two entities using proper annotations.
In ContratoCategoriaCalidad class change field to:
#ManyToOne
#JoinColumn(name = "CONTRATO_ID")
private Contrato contrato;
In Contrato class add field:
#OneToMany(mappedBy = "contrato")
private List<ContratoCategoriaCalidad> relacionContratoCategoriaCalidad;
If you want to enable cascade updates and removals consider adding cascade = CascadeType.ALL and orphanRemoval = true attributes to #OneToMany annotation.
Hope this helps!

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).

How to use existing records in many-to-many relation and to avoid unique constraint violation (hibernate)

There are two classes, Person and Vehicle, in many-to-many relationship. If a new Person is created or a record of existing Person is updated (e.g. Vehicle records added) it would be desirable to use existing Vehicle record if it exists.
The question is how to achieve it. A query prior the insert or update is not an option because there are many threads which can update or insert.
At this moment the application checks unique constraint exception and when it is caught the new existing Vehicle object is replaced by one which is queried from the db by "registration" column. This solution is working however it seems to be kind of clumsy as there has to be a separate session created for each Vehicle record.
Is there any way how to achieve desired behaviour by hibernate annotations? Or completely different solution? Thanks.
#Entity
#Table(name="PERSON", uniqueConstraints = { #UniqueConstraint(columnNames = "name", name="NAME_KEY") })
public class Person implements Serializable {
private static final long serialVersionUID = 3507716047052335731L;
#Id
#GeneratedValue(strategy=GenerationType.SEQUENCE, generator="PersonIdSeq")
#SequenceGenerator( name = "PersonIdSeq", sequenceName="PERSON_ID_SEQ")
private Long id;
#Index(name="PERSON_NAME_IDX")
private String name;
#ManyToMany(targetEntity=Vehicle.class, cascade={CascadeType.PERSIST, CascadeType.MERGE})
#JoinTable(name="PERSON_VEHICLE_LNK", joinColumns=#JoinColumn(name="PERSON_ID"),inverseJoinColumns=#JoinColumn(name="VEHICLE_ID"),
uniqueConstraints = { #UniqueConstraint(columnNames = {"PERSON_ID", "VEHICLE_ID"}, name="person_vehicle_lnk_key")})
#GeneratedValue(strategy=GenerationType.SEQUENCE, generator="PersonVehicleLnkIdSeq")
#SequenceGenerator(name = "PersonVehicleLnkIdSeq", sequenceName="PERSON_VEHICLE_LNK_ID_SEQ")
#CollectionId(columns = #Column(name="ID"), type=#Type(type="long"), generator = "PersonVehicleLnkIdSeq")
private List<Vehicle> vehicle = new ArrayList<>();
...
#Entity
#Table( name = "VEHICLE", uniqueConstraints = {#UniqueConstraint(columnNames="registration", name="REGISTRATION_KEY")} )
public class Vehicle implements Serializable {
private static final long serialVersionUID = -5592281235230216382L;
#Id
#GeneratedValue(strategy=GenerationType.SEQUENCE, generator="VehicleIdSeq")
#SequenceGenerator( name = "VehicleIdSeq", sequenceName="VEHICLE_ID_SEQ")
private Long id;
#Index(name="REGISTRATION_IDX")
private String registration;
...
A query prior the insert or update is not an option because there are many threads which can update or insert.
But this is how to do it.
If it is a performance problem (I don't think so) then consider about using a 2nd level cache. The first level can't handle this because it is bound to a session and you need at least one session per thread.
And then you need a version column in both Person and Vehicle.
In your application you already have the following problem: 1. User A loads a Person record. 2. User B loads the same Person record. 3. User A modifies the telephone number and saves the Person record. 4. User B modifies the Address and also saves the Person record. => Result: The modification (telephone number change) of User A is overwritten, the record has the old telephone number and nobody gets informed about this problem.
A version column avoids this problem. When there is a version column, in the step 4 Hibernate finds out the record was modified in meantime and throws an exception. This exception must be caught and User B must be told to reload the record and redo his address change. This forces a little extra work from user B (not much because this case seldom happens), but no information get lost and the database contains the correct information.
The same you have to do when no record was found on first reading, but on insert a constraint violation is caught. You already catch this error, but you don't inform the user, which you probably should do.
There is no easy solution in Hibernate level for this because the application logic has to treat this case (for example with informing the user).

Categories

Resources