After searching for quite a long time I'm wondering if my code is wrong or if it's simply impossible in Hibernate.
I'll use a fake example to explain my problem. Let's say I have three tables in my database, post, posttag and tag. A post can have multiple tags and a tag can be used my multiple posts, so it's a many-to-many association but there's also extra columns in the table between (posttag).
example entity relationship diagram
So, in my code, I have 3 entity and one more class for the composite key of posttag.
PostEntity:
#Entity
#Table(name = "post", catalog = "fakeExample")
public class PostEntity implements Serializable
{
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "IDPOST", nullable = false)
private Integer idpost;
#OneToMany(mappedBy = "post", targetEntity = PostTagEntity.class, cascade = CascadeType.ALL, orphanRemoval = true)
private List<PostTagEntity> listOfPostTag = new ArrayList<>();
public void setIdpost(Integer idpost)
{
this.idpost = idpost;
}
public Integer getIdpost()
{
return this.idpost;
}
public void setListOfPostTag(List<PostTagEntity> listOfPostTag)
{
if (listOfPostTag != null)
{
this.listOfPostTag.clear();
this.listOfPostTag.addAll(listOfPostTag);
}
}
public List<PostTagEntity> getListOfPostTag()
{
return this.listOfPostTag;
}
}
TagEntity:
#Entity
#Table(name = "tag", catalog = "fakeExample")
public class TagEntity implements Serializable
{
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "IDTAG", nullable = false)
private Integer idtag;
#OneToMany(mappedBy = "tag", targetEntity = PostTagEntity.class, cascade = CascadeType.ALL, orphanRemoval = true)
private List<PostTagEntity> listOfPostTag = new ArrayList<>();
public void setIdtag(Integer idtag)
{
this.idtag = idtag;
}
public Integer getIdtag()
{
return this.idtag;
}
public void setListOfPostTag(List<PostTagEntity> listOfPostTag)
{
if (listOfPostTag != null)
{
this.listOfPostTag.clear();
this.listOfPostTag.addAll(listOfPostTag);
}
}
public List<PostTagEntity> getListOfPostTag()
{
return this.listOfPostTag;
}
}
PostTagEntity:
#Entity
#Table(name = "posttag", catalog = "fakeExample")
public class PostTagEntity implements Serializable
{
#EmbeddedId
private PostTagEntityKey compositePrimaryKey = new PostTagEntityKey();
#Column(name = "EXTRACOLUMN")
private Integer extraColumn;
#ManyToOne
#JoinColumn(name = "IDPOST", referencedColumnName = "IDPOST", nullable = false)
#MapsId("idpost")
private PostEntity post;
#ManyToOne
#JoinColumn(name = "IDTAG", referencedColumnName = "IDTAG", nullable = false)
#MapsId("idtag")
private TagEntity tag;
public void setIdpost(Integer idpost)
{
this.compositePrimaryKey.setIdpost(idpost);
}
public Integer getIdpost()
{
return this.compositePrimaryKey.getIdpost();
}
public void setIdtag(Integer idtag)
{
this.compositePrimaryKey.setIdtag(idtag);
}
public Integer getIdtag()
{
return this.compositePrimaryKey.getIdtag();
}
public void setExtraColumn(Integer extraColumn)
{
this.extraColumn = extraColumn;
}
public Integer getExtraColumn()
{
return this.extraColumn;
}
public void setPost(PostEntity post)
{
this.post = post;
}
public PostEntity getPost()
{
return this.post;
}
public void setTag(TagEntity tag)
{
this.tag= tag;
}
public TagEntity getTag()
{
return this.tag;
}
}
PostTagEntityKey:
#Embeddable
public class PostTagEntityKey implements Serializable
{
#Column(name = "IDPOST", nullable = false)
private Integer idpost;
#Column(name = "IDTAG", nullable = false)
private Integer idtag;
public PostTagEntityKey()
{
}
public PostTagEntityKey(Integer idpost, Integer idtag)
{
this.idsalle = idsalle;
this.idequipement = idequipement;
}
public void setIdpost(Integer value)
{
this.idpost = value;
}
public Integer getIdpost()
{
return this.idpost;
}
public void setIdtag(Integer value)
{
this.idtag = value;
}
public Integer getIdtag()
{
return this.idtag;
}
public boolean equals(Object obj)
{
if (this == obj)
{
return true;
}
if (obj == null)
{
return false;
}
if (this.getClass() != obj.getClass())
{
return false;
}
PostTagEntityKey other = (PostTagEntityKey) obj;
if (this.idpost == null ? other.idpost != null : !this.idpost.equals((Object) other.idpost))
{
return false;
}
if (this.idpost == null ? other.idpost != null
: !this.idtag.equals((Object) other.idtag))
{
return false;
}
return true;
}
public int hashCode()
{
final int prime = 31;
int result = 1;
result = prime * result + ((idpost == null) ? 0 : idpost.hashCode());
result = prime * result + ((idtag == null) ? 0 : idtag.hashCode());
return result;
}
}
Also, I am using Spring, so here are the few class involved when I do an insert or something else. I don't think the problem come from here but just in case.
PostService:
public interface PostService
{
public List<PostEntity> findAll();
public Optional<PostEntity> findById(int var1);
public PostEntity save(PostEntity var1);
public void deleteById(int var1);
}
PostImpl:
#Service
public class PostImpl implements PostService
{
#Autowired
private PostRepository repository;
#Override
public List<PostEntity> findAll()
{
return this.repository.findAll();
}
#Override
public Optional<PostEntity> findById(int id)
{
return this.repository.findById(id);
}
#Override
public PostEntity save(PostEntity toSave)
{
return (PostEntity) this.repository.save(toSave);
}
#Override
public void deleteById(int id)
{
this.repository.deleteById(id);
}
}
PostRepository:
#Repository
public interface PostRepository extends JpaRepository<PostEntity, Integer>, JpaSpecificationExecutor<PostEntity>, PagingAndSortingRepository<PostEntity, Integer>
{
}
So when I need to insert a post, I just use something like this:
#Autowired
PostService postService;
public PostEntity createPost(PostEntity post)
{
return this.postService.save(post);
}
For me, the expected behaviour of Hibernate would be:
when I insert a post, to insert the post and insert every postTag in listOfPostTag
when I update a post, to remove every missing postTag in listOfPostTag, to add every new postTag in listOfPostTag, to update the change postTag in listOfPostTag and to update the post
when I delete a post, to delete every postTag in listOfPostTag and to delete the post
However, when I try to insert a post, I have an error. And from the many tests I've done, it seems that Hibernate insert the post successfully and then tries to insert the postTags, but fails because the idPost in PostTagEntityKey is still null. I would have expected that Hibernate updated it with the id from the inserted post.
So my question is can hibernate do that in the case I described? Or do I have to do it by hand (by not using the cascade mode for insert/update)?
The explanation might be that it's impossible with composite keys, bidirectional, something else or that it's just not something hibernate is supposed to do. I'd like to know if it's possible and if it is, what did I do wrong?
If it's not possible, I wonder what is the point of inserting things in cascade if you can't even do it for a thing as common as an intermediary table.
I haven't tried to code this fake example but I believe it would have the same result as I changed almost nothing from the original. Also I skipped the part where I create the postEntity because in my case it's parsed from JSON. I used the debugger and tried different things, so I'm almost sure the problem doesn't come from here. Every field is filled, even the idTag in PostTagEntityKey. It's just the idPost in PostTagEntityKey that is null because the post hasn't been inserted yet. And hibernate doesn't update it after inserting the post. I start to believe there's no way for the cascade mode to update it and maybe it's the case.
EDIT :
So, thanks to the comment of #a.ghavidel, I realised I've never tried to set the post in posttag.
So I've changed the function setListOfPostTag in post entity like this :
public void setListOfPostTag(List<PostTagEntity> listOfPostTag)
{
if (listOfPostTag != null)
{
this.listOfPostTag.clear();
for(PostTagEntity postTag : listOfPostTag)
{
postTag.setPost(this);
this.listOfPostTag.add(postTag);
}
}
}
and the problem has changed as well. Before that modification, it was telling me that it couldn't insert postTag because idPost was null. And now it's telling me "org.hibernate.PersistentObjectException: detached entity passed to persist: com.fakeexample.TagEntity".
So I think, to retreive the primary key, the entity postTag needed the variable Post to be set. The List listOfPostTag in Post wasn't enough. So my original problem is now fixed I think.
My new problem is that hibernate seems to consider that the tag in postTag is "detached" and I'm not sure what that mean.
In my case, the tag in postTag doesn't come from a function of hibernate, it has been parsed from json so maybe that's why. However I don't need it to be persisted, in theory I just need its id to insert the postTag and there is no cascade in postTag.
I've tried replacing CascadeType.All with CascadeType.Merge cause some people said it worked for them but when I do this it just doesn't insert any postTag when I insert a post. However it seems to work very well when I update a post.
I think I'm very close to the solution. I'm going to try a few things and I will edit this post if I find the answer.
EDIT 2 :
So, I've tried a few things and made some progress. The object is detached because it hasn't been created by hibernate. There is no problems during a merge but it can't be persisted.
A solution might be to do everything by hand in the create function...
But the proper solution would be to use the function getReference() from entityManager. It doesn't generate any unwanted select or update, it just create a proxy object and only require the id in parameter.
However the entityManager is not accessible in spring I think, but we can use the function getOne() from a repository which is mapped to the getReference() method. Basically, if I understood correctly, the function getOne() is supposed to be using lazy loading, so the object isn't loaded as long as we don't need it to be loaded.
I tried to use this function and indeed my code is now working correctly.
But the problem is : the function getOne() is deprecated.
The function has been replaced by getById() but I'm really not sure it use lazy loading too because I'm already using this function a lot and not to create proxy object at all. Also, I know the attribute "fetch = FetchType.LAZY" cant be put in #OneToMany, so, if I didn't put it in my code I suppose I'm not using lazy loading and it will do a lot of unwanted select. Also I don't think I should be using lazy loading all the time neither, I heard it can be troublesome by sometime generating one select for each entity of a collection instead of a single select with lazy loading...
So I still need to make some research to know how to do it with non derprecated function.
EDIT 3 :
Okay so no, the function I was using was findById and not getById. So getById is probably the solution to my problem. I'm gona check the doc to confirm and test it out.
FINAL EDIT :
Well it's working for the insert but I have now a problem during the update. Basically it tells me the posttag already exists in the DB. Probably because I replace the listOfPostTag from the findById with a list I created myself by parsing it. The solution is probably to edit the objects in this list. Now that I understand how the things work in hibernate I need to review the entirety of my code to apply these principles.
In the end, it seems that hibernate is not very friendly restful apis and we have to process every object so hibernate can recognize them.
To summerize, my problems were :
first : consistency. I didn't set the post in postTag object.
Second : attach and detach objects. I didn't used getById function to create an Object recognized by hibernate for the Taf Object.
Third : updating. When I updated a post, I didn't update the objects in listOfPostTag, I just replaced it with another list, with only objects not recognized by hibernate. I should have updated the list Object by Object I guess.
You should use this method in PostEntity when you are creating one
private void addPostTag(PostTagEntity postTag){
postTag.setPost(this);
this.listOfPostTag.add(postTag);
}
I think this will fix your problem
Related
I've been trying to update from Hibernate 5.3.18 / Spring JPA 2.1.21 to Hibernate 6.1.5 / Spring JPA 3.0.0. Everything worked well before this update. Some simple tests that have worked so far now fail. Here is one of them that gives me headaches.
Let's start with an entity (I'll skip a thing or two to keep it simple):
#Entity
#Table(name="my_entity")
public class MyEntity implements Serializable {
#Id
#Column(unique = true, nullable = false)
private Long id;
protected MyEmbeddable myEmbeddable;
...
/* Some other stuff like equals() and hashcode() */
...
public MyEmbeddable getMyEmbeddable() {
return myEmbeddable;
}
public void setMyEmbeddable(MyEmbeddable myEmbeddable) {
this.myEmbeddable = myEmbeddable;
}
}
#Embeddable
public class MyEmbeddable implements Serializable {
#Column(name = "boolean_value", precision = 1)
#Convert(converter = BooleanConverter.class)
private Boolean booleanValue;
public Boolean getBooleanValue() {
return booleanValue;
}
public void setBooleanValue(Boolean booleanValue) {
this.booleanValue = booleanValue;
}
}
Note that a BooleanConverteris defined to map 1/0 to true/false (legacy database).
#Repository
public interface MyEntityRepository extends JpaRepository<MyEntity, Long> {
#Query("SELECT e FROM MyEntity e "
+ "WHERE e.myEmbeddable.booleanValue = :booleanValue")
List<MyEntity> findByBooleanValue(#Param("booleanValue") Boolean booleanValue);
/* The following methods were added for new tests after upgrade. They won't be kept. */
List<MyEntity> findByMyEmbeddableBooleanValue(Boolean booleanValue);
List<MyEntity> findByMyEmbeddableBooleanValueIsTrue();
List<MyEntity> findByMyEmbeddableBooleanValueIsFalse();
}
Now, before the update, MyEntityRepository.findByBooleanValue() was working perfectly. I created the following test:
A MyEntity entity was created. Its MyEmbeddable object was created with booleanValue = true.
The entity was saved in a database.
A test checked that MyEntityRepository.findByBooleanValue(true) was returning a list with one entity.
Since the update, this test failed. What I've observed so far when debugging this test:
Calling MyEntityRepository.findByBooleanValue(true) returns an empty list.
Calling MyEntityRepository.findAll() returns a list with the entity. In its embedded object, booleanValue = true.
Calls to MyEntityRepository.findByMyEmbeddableBooleanValue(true) or MyEntityRepository.findByMyEmbeddableBooleanValueIsTrue() return empty lists.
Calls to MyEntityRepository.findByMyEmbeddableBooleanValue(false) or MyEntityRepository.findByMyEmbeddableBooleanValueIsFalse() return a list with the entity (with myEmbeddable.booleanValue = true).
And, to make it more complicated, if I change the test and set myEntity.myEmbeddable.booleanValue = false, the test runs with success.
My question: what am I missing here? Why can't true work in these tests? Any clues?
Regards
Francois
I have an TimelineEntity entity, that uses HoTimelineType enum with custom integer value. That custom integer value is stored in the database. Implemented via Using #PostLoad and #PrePersist Annotations
Sprint JPA Repository is used to save and get entities.
Here is the issue:
#Entity
#Table(name = TABLE_NAME)
#IdClass(TimelineKey.class)
public class TimelineEntity {
public interface Persistence {
String TABLE_NAME = "timelines";
}
#Id
#Column(name = "node_id")
private Long nodeId;
#Id
#Column(name = "timeline_id")
private Long timelineId;
#Column(name = "ho_timeline_type")
private Integer hoTimelineTypeValue;
#Transient
private HoTimelineType hoTimelineType;
public Long getNodeId() {
return nodeId;
}
public void setNodeId(Long nodeId) {
this.nodeId = nodeId;
}
public Long getTimelineId() {
return timelineId;
}
public void setTimelineId(Long timelineId) {
this.timelineId = timelineId;
}
public HoTimelineType getHoTimelineType() {
return hoTimelineType;
}
public void setHoTimelineType(HoTimelineType hoTimelineType) {
this.hoTimelineType = hoTimelineType;
}
public Integer getHoTimelineTypeValue() {
return hoTimelineTypeValue;
}
public void setHoTimelineTypeValue(Integer hoTimelineTypeValue) {
this.hoTimelineTypeValue = hoTimelineTypeValue;
}
#PostLoad
private void postLoad() {
this.hoTimelineType = HoTimelineType.of(hoTimelineTypeValue);
}
#PrePersist
private void prePersist() {
this.hoTimelineTypeValue = hoTimelineType.getValue();
}
}
#Eager
public interface TimelineEntityRepository extends JpaRepository<TimelineEntity, TimelineKey> {
List<TimelineEntity> findByNodeId(Long nodeId);
}
#Autowired
private TimelineEntityRepository timelineEntityRepository;
...
TimelineEntity newTE = new TimelineEntity();
newTE.setNodeId(10L);
newTE.setTimelineId(22L);
newTE.setHoTimelineType(HoTimelineType.TYPE_1);
newTE = timelineEntityRepository.save(newTE);
When the newTE entity is saved, prePersist is invoked, and inside this method, the hoTimelineType is null and I get NPE. nodeId and timelineId are not nulls. If I stay with a debugger on the last line, outside of prePersist, I see that hoTimelineType has the value, I set before.
When I load entities, inserted with test data, everything works fine and both hoTimelineType and hoTimelineTypeValue have not nullable values.
I skipped the code of TimelineKey and HoTimelineType to simplify the example. Can add it, if needed.
What could reset hoTimelineType? What do I miss?
It seems there is no way to control the saving behaviour of spring jpa repository proxy.
Possible solutions for issue:
Via javax.persistence.Converter. It is pretty clear, the structe of an entity is simple. Can confirm it works fine with Spring Jpa Repository generation.
Explicitely set hoTimelineTypeValue before you save an entity. Error-prone solution. Everytime you save an entity you must think about the difference between the hoTimelineTypeValue and hoTimelineType.
You could enrich setters and getters of the entity class, to explicitely control the consistency between the fields. It makes implementation of entity classes not so obvious. You get more compicated solution for nothing. As a result error-prone solution. Do not recommend it as well.
Cause of disadvantages of #2 and #3 I do not provide examples. It makes no sense.
Example of the solution #1 can be found here: Using JPA 2.1 #Converter Annotation
In our spring boot application, I am trying to save an aggregate, that consists of a root entity (ParentEntity) and a Set of child entities (ChildEntity).
The intention is, that all operations are done through the aggreate. So there is no need for a repository for ChildEntity, as the ParentEntity is supposed to manage all save or update operations.
This is how the Entities look like:
#Entity
#Table(name = "tab_parent", schema = "test")
public class ParentEntity implements Serializable {
#Id
#Column(name = "parent_id")
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer parentId;
#Column(name = "description")
private String description;
#Column(name = "created_datetime", updatable = false, nullable = false)
#ColumnTransformer(write = "COALESCE(?,CURRENT_TIMESTAMP)")
private OffsetDateTime created;
#Column(name = "last_modified_datetime", nullable = false)
#ColumnTransformer(write = "COALESCE(CURRENT_TIMESTAMP,?)")
private OffsetDateTime modified;
#OneToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL, orphanRemoval = true, mappedBy = "ParentEntity")
private Set<ChildEntity> children;
// constructor and other getters and setters
public void setChildren(final Set<ChildEntity> children) {
this.children = new HashSet<>(children.size());
for (final ChildEntity child : children) {
this.addChild(child);
}
}
public ParentEntity addChild(final ChildEntity child) {
this.children.add(child);
child.setParent(this);
return this;
}
public ParentEntity removeChild(final ChildEntity child) {
this.children.add(child);
child.setParent(null);
return this;
}
}
#Entity
#DynamicUpdate
#Table(name = "tab_child", schema = "test")
public class ChildEntity implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "child_id")
private Integer childId;
#Column(name = "language_id")
private String languageId;
#Column(name = "text")
private String text;
#Column(name = "created_datetime", updatable = false, nullable = false)
#ColumnTransformer(write = "COALESCE(?,CURRENT_TIMESTAMP)")
public OffsetDateTime created;
#Column(name = "last_modified_datetime", nullable = false)
#ColumnTransformer(write = "COALESCE(CURRENT_TIMESTAMP,?)")
public OffsetDateTime modified;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "parent_id", updatable = false)
private ParentEntity parent;
// constructor and other getters and setters
public ParentEntity getParent() {
return this.parent;
}
public void setParent(final ParentEntity parent) {
this.parent = parent;
}
}
This is the store method to save or update the entities:
public Integer merge(final ParentDomainObject parentDomainObject) {
final ParentEntity parentEntity =
this.mapper.toParentEntity(parentDomainObject);
final ParentEntity result = this.entityManager.merge(parentEntity);
this.entityManager.flush();
return result.getParentId();
}
And this is the store method to retrieve the aggregate by id:
public Optional<ParentDomainObject> findById(final Integer id) {
return this.repo.findById(id).map(this.mapper::toParentDomainObject);
}
As you can see our architecture strictly separates the store from the service layer. So the service only knows about domain objects and does not depend on Hibernate Entites at all.
When updating either the child or the parent, firstly the parent is loaded. In the service layer, the domain object is updated (fields are set, or a child is added/removed).
Then the merge method (see code snippet) of the store is called with the updated domain object.
This works, but not completely as we want to. Currently every update leads to the parent and EVERY chhild entity being saved, even if all field remained the same. We added the #DynamicUpdate annotaton. Now we saw, that the "modified" field is the problem.
We use a #ColumnTransformer to have the database set the date. Now even if you call the services update method without changing anything, Hibernate generates a update query for EVERY object, which updates only the modified field.
The worst thing about that is, as every object is saved, every modified date changed as well to the current date. But we need information about exactly which object really changed and when.
Is there any way to tell hibernate, that this column should not be taken into account when deciding what to update. However of course, if a field changed, the update operation should indeed update the modified field.
UPDATE:
My second approach after #Christian Beikov mentioned the use of #org.hibernate.annotations.Generated( GenerationTime.ALWAYS )
is the following:
Instead of #Generated (which uses #ValueGenerationType( generatedBy = GeneratedValueGeneration.class )),
I created my own annotations, which use custom AnnotationValueGeneration implementations:
#ValueGenerationType(generatedBy = CreatedTimestampGeneration.class)
#Retention(RetentionPolicy.RUNTIME)
public #interface InDbCreatedTimestamp {
}
public class CreatedTimestampGeneration
implements AnnotationValueGeneration<InDbCreatedTimestamp> {
#Override
public void initialize(final InDbCreatedTimestamp annotation, final Class<?> propertyType) {
}
#Override
public GenerationTiming getGenerationTiming() {
return GenerationTiming.INSERT;
}
#Override
public ValueGenerator<?> getValueGenerator() {
return null;
}
#Override
public boolean referenceColumnInSql() {
return true;
}
#Override
public String getDatabaseGeneratedReferencedColumnValue() {
return "current_timestamp";
}
}
#ValueGenerationType(generatedBy = ModifiedTimestampGeneration.class)
#Retention(RetentionPolicy.RUNTIME)
public #interface InDbModifiedTimestamp {
}
public class ModifiedTimestampGeneration
implements AnnotationValueGeneration<InDbModifiedTimestamp> {
#Override
public void initialize(final InDbModifiedTimestamp annotation, final Class<?> propertyType) {
}
#Override
public GenerationTiming getGenerationTiming() {
return GenerationTiming.ALWAYS;
}
#Override
public ValueGenerator<?> getValueGenerator() {
return null;
}
#Override
public boolean referenceColumnInSql() {
return true;
}
#Override
public String getDatabaseGeneratedReferencedColumnValue() {
return "current_timestamp";
}
}
I use these annotations in my entities instead of the #ColumnTransformer annotations now.
This works flawlessly when I insert a new ChildEntity via addChild(), as now not all timestamps of all entities of the aggregate are updated anymore. Only the timestamps of the new child are set now.
In other words, the InDbCreatedTimestamp works as it should.
Sadly, the InDbModifiedTimestamp does not. Because of GenerationTiming.ALWAYS, I expected the timestamp to be generated on db level, everytime an INSERT OR UPDATE is issued. If I change a field of a ChildEntity and then save the aggregate, an update statement is generated only for this one database row, as expected. However, the last_modified_datetime column is not updated, which is surprising.
It seems that this is unfortunately still an open bug. This issue describes my problem precisely: Link
Can someone provide a solution how to get this db function executed on update as well (without using db triggers)
You could try to use #org.hibernate.annotations.Generated( GenerationTime.ALWAYS ) on these fields and use a database trigger or default expression to create the value. This way, Hibernate will never write the field, but read it after insert/update.
Overall this has a few downsides though (need the trigger, need a select after insert/update), so I think this is a perfect use case for Blaze-Persistence Entity Views.
I created the library to allow easy mapping between JPA models and custom interface or abstract class defined models, something like Spring Data Projections on steroids. The idea is that you define your target structure(domain model) the way you like and map attributes(getters) via JPQL expressions to the entity model.
A DTO/domain model for your use case could look like the following with Blaze-Persistence Entity-Views:
#EntityView(ParentEntity.class)
#UpdatableEntityView
public interface ParentDomainObject {
#IdMapping
Integer getParentId();
OffsetDateTime getModified();
void setModified(OffsetDateTime modified);
String getDescription();
void setDescription(String description);
Set<ChildDomainObject> getChildren();
#PreUpdate
default preUpdate() {
setModified(OffsetDateTime.now());
}
#EntityView(ChildEntity.class)
#UpdatableEntityView
interface ChildDomainObject {
#IdMapping
Integer getChildId();
String getName();
}
}
Querying is a matter of applying the entity view to a query, the simplest being just a query by id.
ParentDomainObject a = entityViewManager.find(entityManager, ParentDomainObject.class, id);
The Spring Data integration allows you to use it almost like Spring Data Projections: https://persistence.blazebit.com/documentation/entity-view/manual/en_US/index.html#spring-data-features
Page<ParentDomainObject> findAll(Pageable pageable);
The best part is, it will only fetch the state that is actually necessary! It also supports writing/mapping back to the persistence model in an efficient manner. Since it does dirty tracking for you, it will only flush changes if the object is actually dirty.
public Integer merge(final ParentDomainObject parentDomainObject) {
this.entityViewManager.save(this.entityManager, parentDomainObject);
this.entityManager.flush();
return parentDomainObject.getParentId();
}
So after some research, I could not find something relevant.
I use one table as below:
#Table(name="product")
public class Product {
#Id
#GeneratedValue
private long productId;
// other irrelevant columns and code goes here
}
Now, I want to create another table that it goes like:
To make that, I tried something like this by following other examples or samples:
#Table(name="combined_products")
public class CombinedProducts {
#EmbeddedId
protected CombinedProductsPK bridgeId;
#ManyToOne
#JoinColumns({
#JoinColumn(name = "product_1", referencedColumnName = "product_id"),
#JoinColumn(name = "product_2", referencedColumnName = "product_id")
})
#Column(name = "notes")
private String notes;
public ProductMatrix() {
bridgeId = new CombinedProductsPK();
}
// irrelevant code again
}
and the CombinedProductsPK:
#Embeddable
public class CombinedProductsPK implements Serializable {
public Long product_1;
public Long product_2;
public CombinedProductsPK() {}
#Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
CombinedProductsPK b = (CombinedProductsPK)obj;
if (b == null) {
return false;
}
return b.product_1.equals(product_1) && b.product_2.equals(product_2);
}
#Override
public int hashCode() {
return (int)(product_1 + product_2);
}
}
and all seems to work perfect.
BUT, my problem is, when I take a look at the database and specific to the combined_products table, there is no FOREIGN_KEY constraint. Is there any way to describe this constraint in Java, or I must manually, in the Java part, take care of this??
This is how my table looks like in the MySQLWorkbench
CREATE TABLE `combined_products` (
`product_1` bigint(20) NOT NULL,
`product_2` bigint(20) NOT NULL,
`notes` varchar(255) DEFAULT NULL,
PRIMARY KEY (`product_1`,`product_2`)
)
I'm in a dead end here, so maybe I follow a wrong route. Every recommendation is accepted! Thanks in advance...
i didn't got what you mean. but maybe you need to try #JoinTable for that ?
enter link description here
i hope it helps.
I've got a transaction that saves or updates a set of objects in a database. This is the code:
#Transactional
public void updatePDBEntry(Set<PDBEntry> pdbEntrySet) {
for (PDBEntry pdbEntry : pdbEntrySet) {
PDBEntry existingEntry = findByAccessionCode(pdbEntry.getAccessionCode());
if (existingEntry != null) {
log.debug("Remove previous version of PDBEntry {}", existingEntry);
makeTransient(existingEntry);
}
makePersistent(pdbEntry);
}
}
And in the genericDAO:
public void makePersistent(I entity) {
getCurrentSession().saveOrUpdate(entity);
}
public void makeTransient(I entity) {
getCurrentSession().delete(entity);
}
Somehow this doesn't work, it says it can't insert the object because of a duplicate key, even though I can see in the logs that it gets to makeTransient(). I guess this has to do with the fact that all this is happening within a transaction, so the changes made by makeTransient() might not be seen by the makePersistent() method. I could solve this by copying all data from pdbEntry into existingEntry and then doing saveOrUpdate(existingEntry) but that is sort of a dirty hack. Is there another way to make sure the makeTransient is visible to makePersistent, while still keeping it all within a transaction?
EDIT: This is my PDBEntry domain model:
#Entity
#Data
#NoArgsConstructor(access = AccessLevel.PROTECTED)
#EqualsAndHashCode(callSuper = false, of = { "accessionCode", "date" })
#SuppressWarnings("PMD.UnusedPrivateField")
public class PDBEntry extends DomainObject implements Serializable {
#NaturalId
#NotEmpty
#Length(max = 4)
private String accessionCode;
#NaturalId
#NotNull
#Temporal(TemporalType.DATE)
private Date date;
private String header;
private Boolean isValidDssp;
#Temporal(TemporalType.TIMESTAMP)
private Date lastUpdated = new Date(System.currentTimeMillis());
#OneToOne(mappedBy = "pdbEntry", cascade = CascadeType.ALL)
private ExpMethod expMethod;
#OneToMany(mappedBy = "pdbEntry", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
private Set<Refinement> refinementSet = new HashSet<Refinement>();
#OneToMany(mappedBy = "pdbEntry", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
private Set<HetGroup> hetGroupSet = new HashSet<HetGroup>();
#OneToMany(mappedBy = "pdbEntry", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
private Set<Chain> chainSet = new HashSet<Chain>();
#OneToMany(mappedBy = "pdbEntry", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
private Set<Chain> residueSet = new HashSet<Chain>();
public Date getLastUpdated() {
return new Date(lastUpdated.getTime());
}
public void setLastUpdated() throws InvocationTargetException {
throw new InvocationTargetException(new Throwable());
}
public void touch() {
lastUpdated = new Date(System.currentTimeMillis());
}
#Override
public String toString() {
return accessionCode;
}
public PDBEntry(String accessionCode, Date date) throws NullPointerException {
if (accessionCode != null && date != null) {
this.accessionCode = accessionCode;
this.date = date;
} else {
throw new NullPointerException();
}
}
}
#MappedSuperclass
public abstract class DomainObject implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
public Long getId() {
return id;
}
#Override
public abstract boolean equals(Object obj);
#Override
public abstract int hashCode();
#Override
public abstract String toString();
}
EDIT: New problem, I've created a method that first deletes all the existing objects from the database like so:
#Override
#Transactional
public void updatePDBEntries(Set<PDBEntry> pdbEntrySet) {
findAndRemoveExistingPDBEntries(pdbEntrySet);
savePDBEntries(pdbEntrySet);
}
#Override
#Transactional
public void findAndRemoveExistingPDBEntries(Set<PDBEntry> pdbEntrySet) {
for (PDBEntry pdbEntry : pdbEntrySet) {
PDBEntry existingPDBEntry = findByAccessionCode(pdbEntry.getAccessionCode());
if (existingPDBEntry != null) {
log.info("Delete: {}", pdbEntry);
makeTransient(existingPDBEntry);
}
}
}
#Override
#Transactional
public void savePDBEntries(Set<PDBEntry> pdbEntrySet) {
for (PDBEntry pdbEntry : pdbEntrySet) {
log.info("Save: {}", pdbEntry);
makePersistent(pdbEntry);
}
}
It seems to delete the first 73 entries it encounters, but then gives an error:
WARN 2010-10-25 14:28:49,406 main JDBCExceptionReporter:100 - SQL Error: 0, SQLState: 23503
ERROR 2010-10-25 14:28:49,406 main JDBCExceptionReporter:101 - Batch entry 0 /* delete nl.ru.cmbi.pdbeter.core.model.domain.PDBEntry */ delete from PDBEntry where id='74' was aborted. Call getNextException to see the cause.
WARN 2010-10-25 14:28:49,406 main JDBCExceptionReporter:100 - SQL Error: 0, SQLState: 23503
ERROR 2010-10-25 14:28:49,406 main JDBCExceptionReporter:101 - ERROR: update or delete on table "pdbentry" violates foreign key constraint "fke03a2dc84d44e296" on table "hetgroup"
Detail: Key (id)=(74) is still referenced from table "hetgroup".
ERROR 2010-10-25 14:28:49,408 main AbstractFlushingEventListener:324 - Could not synchronize database state with session
org.hibernate.exception.ConstraintViolationException: Could not execute JDBC batch update
Any ideas how this error might arise?
EDIT: I found the problem: the last error was caused by the hetgroup model, which is as follows:
#Entity
#Data
#NoArgsConstructor(access = AccessLevel.PROTECTED)
#EqualsAndHashCode(callSuper = false, of = { "pdbEntry", "hetId" })
#SuppressWarnings("PMD.UnusedPrivateField")
// extends DomainObject which contains Id, NaturalId is not enough in this case, since duplicate chains still exist
// in fact this is an error of the PDBFinder and will be fixed in the future
public class HetGroup extends DomainObject implements Serializable {
//#NaturalId
#NotNull
#ManyToOne
private PDBEntry pdbEntry;
//#NaturalId
#NotEmpty
private String hetId;
private Integer nAtom;
#Length(max = 8192)
private String name;
public HetGroup(PDBEntry pdbEntry, String hetId) {
this.pdbEntry = pdbEntry;
pdbEntry.getHetGroupSet().add(this);
this.hetId = hetId;
}
}
Especially pay attention to the commented #NaturalId's, I commented these because there was still some error in my data that caused duplicate hetgroups, so I thought I'd just remove the UNIQUE constraint on them for now, but I forgot that I was using Lombok to create equals and hashcode methods for me, as can be seen from the line:
#EqualsAndHashCode(callSuper = false, of = { "pdbEntry", "hetId" })
This caused the error with the duplicate HetId's. I fixed the data and reinstated the #NaturalId's, and now everything works fine.
Thanks all!
Somehow this doesn't work, it says it can't insert the object because of a duplicate key, even though I can see in the logs that it gets to makeTransient().
To understand what is happening here, you need first to understand that Hibernate doesn't immediately write changes to the database, changes are enqueued in the session and written at flush time. So even if you see that makeTransient() is called, this doesn't mean the corresponding record has been actually deleted from the database at the time the method is called. The SQL delete statement, and other pending change(s) will be executed when the flush will occur (either by explicitly calling flush(), at commit() time, or when performing a HQL query). This is well explained in the documentation:
10.10. Flushing the Session
Sometimes the Session will execute the
SQL statements needed to synchronize
the JDBC connection's state with the
state of objects held in memory. This
process, called flush, occurs by
default at the following points:
before some query executions
from org.hibernate.Transaction.commit()
from Session.flush()
The SQL statements are issued in the
following order:
all entity insertions in the same order the corresponding objects were
saved using session.save()
all entity updates
all collection deletions
all collection element deletions, updates and insertions
all collection insertions
all entity deletions in the same order the corresponding objects were
deleted using Session.delete()
An exception is that objects using
native ID generation are inserted when
they are saved.
...
So, let's look again at your code:
01: #Transactional
02: public void updatePDBEntry(Set<PDBEntry> pdbEntrySet) {
03: for (PDBEntry pdbEntry : pdbEntrySet) {
04: PDBEntry existingEntry = findByAccessionCode(pdbEntry.getAccessionCode());
05: if (existingEntry != null) {
06: log.debug("Remove previous version of PDBEntry {}", existingEntry);
07: makeTransient(existingEntry);
08: }
09: makePersistent(pdbEntry);
10: }
11: }
at 04, you perform a query (this will flush pending changes if any)
at 07, you perform a session.delete() (in memory)
at 09, you perform a session.save() (in memory)
back to 04, changes are flushed and Hibernate issues SQL statements in the above specified order
and the insert fail because of a key violation because the record hasn't been deleted yet
In other words, your problem has nothing to do with transactions, it's just related to the way Hibernate works and the way you use it.
Is there another way to make sure the makeTransient is visible to makePersistent, while still keeping it all within a transaction?
Well, without changing the logic, you could help Hibernate and flush() explicitly after the delete().
But IMO, the whole approach is is suboptimal and you should update the existing record if any instead of delete then insert (unless there are good reasons to do so).
References
Hibernate Core Reference Guide
10.10. Flushing the Session
The single transaction should not be the problem.
Your suggestion - copy the data from pdbEntry into existingEntry is a far better solution. It's a lot less database intensive for one and a little easier to read and understand what is going on.
Also, if you did it this way, you would not be required to do a saveOrUpdate on the changed object. Hibernate implements what is known as "transparent persistence"... which means that hibernate is responsible for working out what data operations need to be employed to synchronise the object with the database. The "Update" operation in hibernate is not for objects that are already persistent. see here
The code would in this case look like this (note not updated required):
public void updatePDBEntry(Set<PDBEntry> pdbEntrySet) {
for (PDBEntry pdbEntry : pdbEntrySet) {
PDBEntry existingEntry = findByAccessionCode(pdbEntry.getAccessionCode());
if (existingEntry != null) {
// copy relevant fields from pdbEntry to existing Entry - preferably with a method on PDBEntry
} else {
makePersistent(pdbEntry); // although better to just call _save_ (not saveOrUpdate) cause you know it's a new object
}
}
}