I have an existing database table For e.g. T_STUDENTS on top of which I have to create a JPA entity. All three columns in the table are NON NULL and the table has a self-reference as mentor_id
id | name | mentor_id
-----|---------|----------
1 | John | 1
-----|---------|----------
2 | Marc | 1
-----|---------|----------
3 | Abby | 2
-----|---------|----------
4 | Jimy | 3
-----|---------|----------
5 | Boni | 4
-----|---------|----------
Each student has a mentor who is also a student. There is a strict OneToOne relationship between the student and the mentor. For id 1, there can't be any mentor, therefore it has the mentor id as it's own id. The ids are generated using a database sequence.
The problem is that while generating the first record with id 1, hibernate is not assigning the same id as mentor id even though I have created necessary relationships. Since columns can't be null and hibernate is not assigning mentor_id, SQLConstraint nonnull exception is thrown.
Following is how I have created the relationship.
#Entity
#Table(name = 'T_STUDENTS')
public class Student implements Serializable {
#Id
#SequenceGenerator(name = 'S_STUDENTS_SEQUENCE', allocationSize = 1)
#GeneratedValue(strategy = GenerationType.SEQUENCE, generator = 'S_STUDENTS_SEQUENCE')
#Column(name = "id")
private Long studentId;
#Column(name = "name", length = 20)
private String studentName;
#OneToOne(optional = false, cascade = CascadeType.NONE)
#JoinColumn(name = "mentor_id")
private Student mentor;
// getters and setters
}
I have set CascadeType.NONE because else hibernate tries to retrieve 2 id's from sequence and tries to create 2 records which are not desirable.
The problem is how can I insert the very first record. Following is how the insert is being done.
Student student = Student.builder()
.setName('John')
.build();
student = student.toBuilder().setMentor(student).build();
return studentRepository.save(student);
If I change the relationship annotation to #ManyToOne since technically mentor_id is 1 is mapped to 2 students, I get the following exception
.InvalidDataAccessApiUsageException: org.hibernate.TransientPropertyValueException: Not-null property references a transient value - transient instance must be saved before current operation
Edit 1: If relationship type changed to #ManyToOne and cascade is removed following error is observed.
org.hibernate.action.internal.UnresolvedEntityInsertActions.logCannotResolveNonNullableTransientDependencies - HHH000437: Attempting to save one or more entities that have a non-nullable association with an unsaved transient entity. The unsaved transient entity must be saved in an operation prior to saving these dependent entities.
Edit 2: Changed the cascade type to cascade = CascadeType.PERSIST and hibernate tries to persist the mentor as a separate record. I verified from logs that it tries to retrieve 2 different sequence ids and creates 2 insert queries, with both mentor_id as null.
NOTE: Finally I found the root cause. I was using Lombok builder in the JPA entity and it does not support the self-reference relationship yet.
I switched to public setters and it worked fine. See the link below for more details
https://github.com/rzwitserloot/lombok/issues/2440#event-3270871969
You can ignore the below solution.
I'm not very proud of the solution, but here is how I achieved it.
1.Removed auto sequence generation from the id.
#Id
#SequenceGenerator(name = 'S_STUDENTS_SEQUENCE', allocationSize = 1)
#GeneratedValue(strategy = GenerationType.SEQUENCE, generator = 'S_STUDENTS_SEQUENCE')
#Column(name = "id")
private Long studentId
to
#Id
#Column(name = "id")
private Long studentId;
2.Changed the mapping to the simple foreign key field.
#OneToOne(optional = false, cascade = CascadeType.NONE)
#JoinColumn(name = "mentor_id")
private Student mentorId;
to
#Column(name = "mentor_id")
private Long mentorId;
3.Created a method to retrieve the sequence manually and then assigned the value to both 'id' and 'mentorId'
#Override
public Student saveExtended(Student student) {
Object sequence =
em.createNativeQuery(
"SELECT NEXT VALUE FOR S_STUDENTS_SEQUENCE AS VALUE FROM SYSIBM.SYSDUMMY1")
.getSingleResult();
BigInteger sequenceLong = (BigInteger) sequence;
student = student.toBuilder().id(sequenceLong.longValue()).mentorId(sequenceLong.longValue()).build();
em.persist(student);
em.flush();
return student;
}
Related
I am creating a Spring 4 / Spring Data application for an existing database. The database structure and data are defined by a closed source software.
One aspect of the existing system is that you can create a comment on any other item in the system. This means, that an article, a document, a media file (all entities in the system) can have any number of comments, and each comment is exactly for one entity in the system. All comments are in the same comment table.
The way this is implemented is that the table comment has a column comment_for that holds a concatenated/namespaced/prefixed reference to the actual entity it is a comment for. The current system seems to just builds the join query by prefixing the primary key with the table name:
+----+-------------------+----------------+
| id | comment_for | comment |
+----+-------------------+----------------+
| 1| article:12345 | This is nice...|
| 2| document:42 | Cool doc! |
+----+-------------------+----------------+
This sample shows two comments, one for an Article with an article.id of 12345 and one for a document with document.id of 42. I created #Entities matching the database tables and the corresponding Repository Interfaces with the query methods I need.
I would like to make use of Spring Data Repositories / Entities to populate the collections of my entities with the corresponding comments, like this (pseudocde) for Entity Article.
#OneToMany(mappedBy = "comment_for", prefix = "article:")
private List<Comment> comment = new ArrayList<>();
I only need it unidirectional. My entities (at the moment Article, Document and Mediafile) should hold a collection of their comments. I don't need comments to hold a reference back to the entity.
Is there a way to do this? The resulting SQL query should be something like
SELECT * FROM .... WHERE comment.comment_for = concat('<entityname>:', <entity>.id);
I looked at #JoinColumn but I can't modify the used value for the join, only the column name. The only solution I have at the moment are manual #Querys on the CommentRepository Interface, which gives me an ArrayList of all comments for a certain Entity / ID combination. But I would like to have the comments automatically joined as part of my Business Entity.
Update : It looks like I am able to split the namespace and id from comment_for into two new columns without interrupting the existing software. The two columns are now comment_for_id and comment_for_entityname
You could also break out comment_for to contain only the id like your entities. Adding an additional column like entity_type would allow you to avoid duplicate id values between different entities.
Also you could use #JoinColumn on the owner side of the relationship between Entity and Comments. It looks like in your case that would be the Comment entity/table, since there are many comments per each entity.
Example:
#Entity
#NamedQueries({ #NamedQuery(name = "Comments.findAll", query = "select o from Comments o") })
#IdClass(CommentsPK.class)
public class Comments implements Serializable {
private static final long serialVersionUID = 4787438687752132432L;
#Id
#Column(name = "COMMENT_TEXT", nullable = false, length = 30)
private String commentText;
#Id
#Column(name = "ENTITY_TYPE", nullable = false, length = 30)
private String entityType;
#ManyToOne
#Id
#JoinColumn(name = "COMMENT_FOR")
private EntityDemo entityDemo;
Note that I set the combination of all three fields as the primary key, I am not sure what criteria is used as the PK in your current set up.
Here is an example of an Entity. The attributes have been made up for the purpose of demonstration.
#Entity
#NamedQueries({ #NamedQuery(name = "EntityDemo.findAll", query = "select o from EntityDemo o") })
#Table(name = "ENTITY_DEMO")
public class EntityDemo implements Serializable {
private static final long serialVersionUID = -8709368847389356776L;
#Column(length = 1)
private String data;
#Id
#Column(nullable = false)
private BigDecimal id;
#OneToMany(mappedBy = "entityDemo", cascade = { CascadeType.PERSIST, CascadeType.MERGE })
private List<Comments> commentsList;
I'm upgrading Hibernate4 to Hibernate5. Spring-4.3.7 And facing the problem that hibernate assigns duplicate IDs to the objects while EntityManager.persist(object) that results in exception :-
org.springframework.dao.DataIntegrityViolationException: A different object with the same identifier value was
already associated with the session : [com.domain.multilanguage.LiteralText#498]; nested exception
is javax.persistence.EntityExistsException: A different object with the same identifier value was already associated with the session : [com.domain.multilanguage.LiteralText#498][machine=]
I debug inside the SharedEntityManagerCreator and found that, hibernate only assigns 0 - 49 objects total 50 ids and after that it start duplicating the ids.
I'm using the sequence generator GenerationType.SEQUENCE.
#Id
#GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "idGenerator")
#SequenceGenerator(name = "idGenerator", sequenceName = "HIBERNATE_SEQUENCE", allocationSize = 50)
#Column(name = "ID")
public long getId() {
return mId;
}
This is working fine with Hibernate4. What could be the problem here, please.
i have situation on which i try to persist entity with id that depends on the max id value, for example the new entity id will be MAX(id)+1.
now i try to use JPA to persist this entity
#Entity
#Table(name = "product")
public class ProductDetails {
#Id
#GeneratedValue
private String id;
i used strategy = GenerationType.AUTO, strategy = GenerationType.IDENTITY,strategy = GenerationType.SEQUENCE,strategy = GenerationType.TABLE none of them work, so i think i can solve it through selecting the max id then +1 and use that value (i did not try it) what i am asking for, is there is any way to handle this situation through JPA or Hibernate.Note:the id columns is not auto-increment and the db doesn't have sequence.
Don't use String as Primary key. if you need id like "ABC123" then take 2 id columns. One as id(int) PK, second as display_id(String). You can auto-generate display_id in database level using trigger.
If you use String as a type of your Id you shouldn't use auto-increment cause String is something that can't be incremented since it's not a number type. Just leave #Idand add #GeneratedValue(generator = "uuid") - that should work.
Additionally you can add #GenericGenerator(name = "uuid", strategy = "uuid2")
I have found that hibernate (or mariadb) JPA does not seem to work with 0 values in a foreign key.
I have a parent class
class Parent {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "PARENT_ID", unique = true, nullable = false)
private Integer parentId;
}
And a child class
class Child {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "CHILD_ID", unique = true, nullable = false)
private Integer childId;
#ManyToOne(fetch = FetchType.EAGER)
#JoinColumn(name = "PARENT_ID", nullable = false)
private Parent parent;
}
So we have 2 rows in parent. parent_id=0 and parent_id=1
My problem is that I get an error when attempting to use parent with ID 0. i.e. This code
Parent p = entityManager.find(Parent.class, new Integer(0));
Child c = new Child();
c.setParent(p);
entityManager.persist(c);
Will fail with the error:
java.lang.IllegalStateException:
org.hibernate.TransientPropertyValueException: Not-null property
references a transient value - transient instance must be saved before
current operation : com.whatever.Child.parent -> com.whatever.Parent
But the following works fine:
Parent p = entityManager.find(Parent.class, new Integer(1));
Child c = new Child();
c.setParent(p);
entityManager.persist(c);
So I assume the PARENT_ID=0 somehow confusing JPA into thinking it is not a valid parent object.
Or is this actually a mariadb issue? Related to the fact that you have to change a session setting in order to insert 0's into AUTO_INCREMENT columns.
Is there any config or annotation I can do to make this work. Unfortunately we are putting JPA code on an existing system, so changing the PARENT_ID values is not a trivial task. (and everybody hates data conversion).
Any tips very much appreciated.
MariaDB/MySQL handle AUTO_INCREMENT thus: Numbers are 1 or greater; 0 is a valid sequence number. If JPA cannot live with those (and some other) limitations, JPA is broken. (Sorry, but I get irritated with 3rd party software that makes life difficult for MySQL users.)
I'm trying to use JPA to generate IDs from sequences in my database (Oracle 9i)
From what I found here and there, here is the group of annotations I've set on my ID variable :
#Id
#SequenceGenerator(name="PROCEDURENORMALE_SEQ_GEN", sequenceName = "PROCEDURENORMALE_SEQ")
#GeneratedValue(strategy=GenerationType.SEQUENCE, generator = "PROCEDURENORMALE_SEQ_GEN")
#Column(name = "IDPROCEDURENORMALE", unique = true, nullable = false, precision = 10, scale = 0)
private long idProcedureNormale;
However, whenever I create a new object, this id is always set to 0, and because of that I can't persist data. I've tried to change the strategy from GenerationType.SEQUENCE to GenerationType.AUTO, nothing changed. For this specific table, Sequence number is supposed to be around 8300.
Where did I go wrong ?
I actually solved my issue, that happened not to be directly related with what I exposed.
This object I was trying to persist is part of a relatively complex object, and in the parent object I didn't add a CascadeType to the JPA mapping annotation :
#OneToMany(fetch = FetchType.LAZY, mappedBy = "dossier")
private Set<Procedurenormale> proceduresNormales = new HashSet<>(0);
Changing this annotation to the following solved the issue :
#OneToMany(fetch = FetchType.LAZY, mappedBy = "dossier", cascade = CascadeType.ALL)
private Set<Procedurenormale> proceduresNormales = new HashSet<>(0);