I have three entities in my application one parameter of which depends on the value of Building entity. Parameter type is dependent on the value of parameter endDate of Building entity. CustomType is an entity stored in another schema of the db.
I understand that this usage of JPA callback is probably breaking conventions and we already have the idea how to implement this other way, but my curiosity won't let me drop this case: what is the cause of different behaviour of Hibernate in those two situations?
I decided to update parameters of Stage and Project via JPA lifecycle event and implemented #PrePersist, #PostUpdate and #PostPersist listeners. This is when I encountered the problem: we are using JSON API to save our entities without explicitly writing code to do so. But for the operation of soft deletion we are using custom RestController, which is saving entity via JPA Repository save() method. When JPA Events are triggered by JSON API everything works correctly - CustomType is changed in all of the entities and persisted to the database. When we are deleting entity and causing changes in type - #PostUpdate callback is working in unexpected way. It seems to persist changes only in Building entity, completely ignoring Stage and Project. After checking via EntityManager.contains() - they are in managed state in the method that is inside of JPA #PostUpdate event, so I expected same behaviour - persist changes without explicitly calling to save() method.
Building.java
#ToString(callSuper = true, onlyExplicitlyIncluded = true)
#EqualsAndHashCode(callSuper = true, onlyExplicitlyIncluded = true)
#NoArgsConstructor
#FieldDefaults(level = AccessLevel.PRIVATE)
#SuperBuilder(toBuilder = true)
#Entity
#Inheritance(strategy = InheritanceType.JOINED)
#Table(name = "BUILDING", schema = "---")
#EntityListeners(BuildingListener.class)
#Where(clause = "DELETED = 0")
public class Building extends BaseEntity {
String name;
String description;
Date endDate;
Project project;
Stage stage;
CustomType type;
#Column(name = "NAME")
public String getName() {
return name;
}
#Column(name = "DESCRIPTION")
public String getName() {
return name;
}
#Column(name = "END_DATE")
public Date getName() {
return name;
}
#ManyToOne(fetch = LAZY)
#JoinColumn(name = "PROJECT_ID")
public Project getProject() {
return project;
}
#ManyToOne(fetch = LAZY)
#JoinColumn(name = "STAGE_ID")
public Stage getStage() {
return stage;
}
#ManyToOne
#JoinColumn(name = "TYPE")
public CustomType getType(){
return type;
}
Project.java
#Data
#ToString(callSuper = true, onlyExplicitlyIncluded = true)
#EqualsAndHashCode(callSuper = true, onlyExplicitlyIncluded = true)
#NoArgsConstructor
#FieldDefaults(level = AccessLevel.PRIVATE)
#SuperBuilder(toBuilder = true)
#Entity
#Inheritance(strategy = InheritanceType.JOINED)
#Table(name = "PROJECT", schema = "---")
#Where(clause = "DELETED = 0")
public class Project extends BaseEntity {
String name;
String description;
List<Stage> stages;
List<Building> buildings;
CustomType type;
#Column(name = "NAME")
public String getName() {
return name;
}
#Column(name = "DESCRIPTION")
public String getName() {
return name;
}
#OneToMany(fetch = LAZY)
public List<Stage> getStages() {
return stages;
}
#OneToMany(fetch = LAZY)
public List<Building> getBuildings() {
return buildings;
}
#ManyToOne
#JoinColumn(name = "TYPE")
public CustomType getType(){
return type;
}
Stage.java
#ToString(callSuper = true, onlyExplicitlyIncluded = true)
#EqualsAndHashCode(callSuper = true, onlyExplicitlyIncluded = true)
#NoArgsConstructor
#FieldDefaults(level = AccessLevel.PRIVATE)
#SuperBuilder(toBuilder = true)
#Entity
#Inheritance(strategy = InheritanceType.JOINED)
#Table(name = "PROJECT", schema = "---")
#Where(clause = "DELETED = 0")
public class Stage extends BaseEntity {
String name;
String description;
Project project;
List<Building> buildings;
CustomType type;
#Column(name = "NAME")
public String getName() {
return name;
}
#Column(name = "DESCRIPTION")
public String getName() {
return name;
}
#ManyToOne(fetch = LAZY)
#JoinColumn(name = "PROJECT_ID")
public Project getProject() {
return project;
}
#OneToMany(fetch = LAZY)
public List<Building> getBuildings() {
return buildings;
}
#ManyToOne
#JoinColumn(name = "TYPE")
public CustomType getType(){
return type;
}
BuildingListener.java
#PrePersist
public void prePersist(Building building){
listenerService.checkTypeForNewBuilding(building);
}
#PreUpdate
public void prePersist(Building building){
listenerService.checkTypeForExistingBuilding(building);
}
#PostUpdate
#PostPersist
public void post(Building building){
listenerService.resolveTypesForRelations(building);
}
}
ListenerService.java
public void checkTypeForNewBuilding(Building building){
CustomType type1 = CustomType.builder().id(/*typeId here*/);
CustomType type2 = CustomType.builder().id(/*typeId here*/);
if (/*logic*/){
building.setType(type1);
} else{
building.setType(type2);
}
}
public void checkTypeForExistingBuilding(Building building){
CustomType type1 = CustomType.builder().id(/*typeId here*/);
CustomType type2 = CustomType.builder().id(/*typeId here*/);
if (/*logic*/){
building.setType(type1);
} else{
building.setType(type2);
}
}
public void resolveTypesForRelations(Building building){
CustomType type1 = CustomType.builder().id(/*typeId here*/);
CustomType type2 = CustomType.builder().id(/*typeId here*/);
Project project = building.getProject();
Stage stage = building.getStage();
if (/*logic*/){
project.setType(type1);
stage.setType(type1);
} else{
project.setType(type2);
stage.setType(type2);
}
}
Related
I have a quite simple problem, yet a very weird behaviour on which a Set does not delete elements on true predicates for some reason.
The entities (for reference only, as this does not have anything to do with the set item - or shouldn't):
#Data
#MappedSuperclass
#EqualsAndHashCode(of = "uuid")
public abstract class Model<ID> {
#Id
#GeneratedValue
protected ID id;
protected UUID uuid; // Hybrid model
#PrePersist
private void onPersisting() {
uuid = UUID.randomUUID();
}
}
#Data
#Entity
#NoArgsConstructor
#EqualsAndHashCode(callSuper = true, of = "name")
#Table(uniqueConstraints = #UniqueConstraint(name = "scope_name", columnNames = "name"))
public class Scope extends Model<Long> {
private String name;
public Scope(UUID uuid) {
this.uuid = uuid;
}
public Scope(String name) {
this.name = name;
}
}
#Data
#Entity
#NoArgsConstructor
#EqualsAndHashCode(callSuper = true, of = "name")
#Table(uniqueConstraints = #UniqueConstraint(name = "role_name", columnNames = "name"))
public class Role extends Model<Long> {
private String name;
#ManyToMany(cascade = { CascadeType.PERSIST, CascadeType.MERGE })
#JoinTable(name = "role_scopes", joinColumns = #JoinColumn(name = "role_id"), inverseJoinColumns = #JoinColumn(name = "scope_id"))
private Set<Scope> scopes = new HashSet<>();
public Role(UUID uuid, Scope... scopes) {
this.scopes = Stream.of(scopes).collect(Collectors.toSet());
this.uuid = uuid;
}
public Role(String name, Scope... scopes) {
this.scopes = Stream.of(scopes).collect(Collectors.toSet());
this.name = name;
}
}
The following snippet, called within a JUnit test case, does not delete the set elements (simplified for readability):
#Transactional
public Role create(Role role) {
role.getScopes().removeIf(unused -> true); // <----
return role;
}
For some reason, that snippet does work:
Set<String> strings = new HashSet<>();
strings.add("FOO");
strings.add("BAR");
strings.removeIf(unused -> true);
What's going on here?
It is because of overridden equals and hashCode that is done by #Data annotation on your Scope class.
If you remove it or replace with #Getter #Setter it will work
I have two entity classes:
#Data
#Entity(name = "user")
#Table(name = "tbl_user")
#EqualsAndHashCode(callSuper = true, exclude = "products")
public class UserEntity extends BaseEntity {
#Column(unique = true)
private String username;
#ToString.Exclude
private String password;
#Transient
#JsonProperty("remember_me")
private Boolean rememberMe;
#Enumerated(EnumType.STRING)
#CollectionTable(name = "tbl_user_role")
#ElementCollection(fetch = FetchType.EAGER)
Set<Role> roles = new HashSet<>();
#OneToMany(mappedBy = "user", fetch = FetchType.EAGER, cascade = CascadeType.ALL)
private List<ProductEntity> products = new ArrayList<>();
}
#Data
#Entity
#Table(name = "tbl_product")
#EqualsAndHashCode(callSuper = true)
public class ProductEntity extends BaseEntity {
#ManyToOne
#JoinColumn(name = "user_id", nullable = false)
private UserEntity user;
private String productName;
}
both extending a baseEntity:
#MappedSuperclass
public abstract class BaseEntity implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
#Version
private long version;
#Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
BaseEntity that = (BaseEntity) o;
return Objects.equals(id, that.id) && Objects.equals(version, that.version);
}
#Override
public int hashCode() {
return Objects.hash(id, version);
}
}
Now when I try to retrieve all products (or all users) (e.g. findAll method) I get a StackOverflowError.
I know this error is caused by the circular dependency between user and products, so I added an exclude to the equals annotation in the userEntity to resolve it, like so: #EqualsAndHashCode(callSuper = true, exclude = "products")
Unfortunately the error keeps popping up. What am I missing here?
In case it might help, lombok also has anotation #EqualsAndHashCode.Exclude very useful for tests!
I managed to fix the issue by adding #ToString.Exclude to the roles and user class variables. Apparently I was printing them somewhere later in the code, which was causing the error.
I have an entity CpoPipeline with a relationship ManyToOne with CpoEnvironment:
#Entity
#Table(name = "cpo_pipeline", catalog = "cup_orchestrator")
public class CpoPipeline implements java.io.Serializable {
private String pipelineId;
private String pipelineName;
private CpoEnvironment cpoEnvironment;
#Column(name = "pipeline_id", unique = true, nullable = false)
public String getPipelineId() {
return this.pipelineId;
}
#ManyToOne(fetch = FetchType.EAGER)
#JoinColumn(name = "environment_id", nullable = false)
public CpoEnvironment getCpoEnvironment() {
return this.cpoEnvironment;
}
//Getters and Setters
}
The entity CpoEnvironment:
#Entity
#Table(name = "cpo_environment", catalog = "cup_orchestrator")
public class CpoEnvironment implements java.io.Serializable {
private String environmentId;
private String environment;
private Set<CpoPipeline> cpoPipelines = new HashSet<CpoPipeline>(0);
#Id
#Column(name = "environment_id", unique = true, nullable = false)
public String getEnvironmentId() {
return this.environmentId;
}
#OneToMany(fetch = FetchType.LAZY, mappedBy = "cpoEnvironment")
public Set<CpoPipeline> getCpoPipelines() {
return this.cpoPipelines;
}
//Getters and Setters
}
The repository for this entity with a method name:
#Repository
public interface PipelineRep extends JpaRepository<CpoPipeline, String> {
Optional<CpoPipeline> findByPipelineIdAndEnvironmentId(String pipelineId, String environmentId);
}
Error: Caused by: org.springframework.data.mapping.PropertyReferenceException: No property environmentId found for type CpoPipeline
How can I create a method name using one field from the entity and one field from the relation? Is it possible?
Yes possible, to use environmentId of CpoEnvironment entity use this way CpoEnvironmentEnvironmentId
Optional<CpoPipeline> findByPipelineIdAndCpoEnvironmentEnvironmentId(String pipelineId, String environmentId);
I have something similar to this:
#Entity
#Table(name = "claim", schema = "test")
public class Claim implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#Column(name = "idClaim", unique = true, nullable = false)
private Integer idClaim;
#OneToOne(mappedBy = "claim", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
#JsonManagedReference
private ClaimReturnInfo claimReturnInfo;
#Column(name = "notes")
private String notes;
// Getters and setters
}
#Entity
#Table(name = "claim_returninfo", schema = "test")
public class ClaimReturnInfo implements Serializable {
#Id
#Column(name = "Claim_idClaim")
private Integer id;
#MapsId("Claim_idClaim")
#OneToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "Claim_idClaim")
#JsonBackReference
private Claim claim;
#Column(name = "description")
private String description;
// Getters and setters
}
ClaimReturnInfo Id is not autogenerated because we want to propagate the Id from its parent (Claim). We are not able to do this automatically and we are getting this error: ids for this class must be manually assigned before calling save() when 'cascade' is executed in ClaimReturnInfo .
Is it possible to map Claim Id into ClaimReturnInfo Id or should we do this manually?
Even if we set this ID manually on claimReturnInfo and we can perform updates, we still get this error when trying to create a new Claim:
// POST -> claimRepository.save() -> Error
{
"notes": "Some test notes on a new claim",
"claimReturnInfo": {
"description": "Test description for a new claimReturnInfo"
}
}
In the ServiceImplemetation:
#Override
#Transactional
public Claim save(Claim claim) throws Exception {
if(null != claim.getClaimReturnInfo()) {
claim.getClaimReturnInfo().setId(claim.getIdClaim());
}
Claim claimSaved = claimRepository.save(claim);
return claimSaved;
}
I have tried using the following mappings and from your comments it was apparent that Json object is populated correctly.
I have noticed that the annotation #MapsId is the culprit.If you check the documentation of #MapsId annotation it says
Blockquote
The name of the attribute within the composite key
* to which the relationship attribute corresponds. If not
* supplied, the relationship maps the entity's primary
* key
Blockquote
If you change #MapsId("Claim_idClaim") to #MapsId it will start persisting your entities.
import javax.persistence.*;
#Entity
#Table(name = "CLAIM")
public class Claim {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#Column(name = "idClaim", unique = true, nullable = false)
private Long idClaim;
#Column(name = "notes")
private String notes;
#OneToOne(mappedBy = "claim", cascade = CascadeType.ALL, optional = false)
private ClaimReturnInfo claimReturnInfo;
public Long getIdClaim() {
return idClaim;
}
public String getNotes() {
return notes;
}
public void setNotes(String notes) {
this.notes = notes;
}
public ClaimReturnInfo getClaimReturnInfo() {
return claimReturnInfo;
}
public void setClaimReturnInfo(ClaimReturnInfo claimReturnInfo) {
if (claimReturnInfo == null) {
if (this.claimReturnInfo != null) {
this.claimReturnInfo.setClaim(null);
}
} else {
claimReturnInfo.setClaim(this);
}
this.claimReturnInfo = claimReturnInfo;
}
}
package com.hiber.hiberelations;
import javax.persistence.*;
#Entity
#Table(name = "CLAIM_RETURN_INFO")
public class ClaimReturnInfo {
#Id
#Column(name = "Claim_idClaim")
private Long childId;
#Column(name = "DESCRIPTION")
private String description;
#MapsId
#OneToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "Claim_idClaim")
private Claim claim;
public Long getChildId() {
return childId;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public Claim getClaim() {
return this.claim;
}
public void setClaim(Claim claim) {
this.claim = claim;
}
}
I write my first java application to read rss stream and use spring, spring-data, hibernate.
My models.
RssFeed:
#Entity(name = "RssFeed")
#Table(name = "FEED")
#JsonIgnoreProperties({"rssChannel"})
public class RssFeed {
#Id
#GeneratedValue
#Column
private Integer id;
#Column(unique = true)
#Index(name = "title_index")
private String title;
#Column
#URL
private String link;
#Column
private String description;
#Column
private String content;
#Column
#Temporal(TemporalType.TIMESTAMP)
private Date pubDate;
#Column
#Temporal(TemporalType.TIMESTAMP)
private Date updateDate;
#ManyToOne
#JoinColumn(name = "channelId")
private RssChannel rssChannel;
#ManyToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
#JoinTable(name = "feed_category",
joinColumns = {#JoinColumn(name = "feed_id", nullable = false, updatable = false)},
inverseJoinColumns = {#JoinColumn(name = "category_id", nullable = false, updatable = false)})
private Set<RssCategory> rssCategories = new LinkedHashSet<RssCategory>();
}
RssChannel:
#Entity(name = "RssChannel")
#Table(name = "Channel",
uniqueConstraints = #UniqueConstraint(columnNames = {"link"}))
#JsonIgnoreProperties({"feeds"})
public class RssChannel implements Serializable{
#Id
#GeneratedValue
#Column
private Integer id;
#Column
private String title;
#Column(unique = true)
#org.hibernate.validator.constraints.URL
private String link;
#Column
#org.hibernate.validator.constraints.URL
private String image;
#Column
private String description;
#OneToMany(mappedBy = "rssChannel", cascade = CascadeType.ALL, orphanRemoval = true)
private List<RssFeed> feeds = new LinkedList<RssFeed>();
}
And RssCategory:
#Entity(name = "RssCategory")
#Table(name = "CATEGORY")
#JsonIgnoreProperties({"rssFeeds"})
public class RssCategory {
#Id
#GeneratedValue
#Column
private Integer id;
#Column(unique = true)
private String title;
#ManyToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER, mappedBy = "rssCategories")
public Set<RssFeed> rssFeeds = new LinkedHashSet<RssFeed>();
}
I use CrudRepository for manipulation with data. When save RssFeed without many to many it`s ok:
RssChannel channel = rssChannelService.get(url.toString());
rssFeed.setRssChannel(channel);
rssFeedService.save(rssFeed);
But when i add RssCategory:
rssCategory rssCategory = rssCategoryService.findOrCreate("test");
rssFeed.getRssCategories().add(rssCategory);
rssFeedService.save(rssFeed);
get exception: rg.hibernate.PersistentObjectException: detached entity passed to persist: RssCategory.
My RssFeedServiceImpl:
#Service
public class RssFeedServiceImpl implements RssFeedService {
#Autowired
private RssChannelDAO rssChannelDAO;
#Autowired
private RssFeedDAO rssFeedDAO;
#Override
public Page<RssFeed> findAll(Pageable pageable) {
return rssFeedDAO.findAll(pageable);
}
#Override
public Page<RssFeed> findAll(int rssChannelId, Pageable pageable) {
RssChannel rssChannel = rssChannelDAO.findOne(rssChannelId);
return rssFeedDAO.findByRssChannel(rssChannel, pageable);
}
#Override
public RssFeed get(String title) {
return rssFeedDAO.findByTitle(title);
}
#Override
public RssFeed save(RssFeed rssFeed) {
return rssFeedDAO.save(rssFeed);
}
}
And RssCategoryServiceImpl:
#Service
public class RssCategoryServiceImpl implements RssCategoryService {
#Autowired
RssCategoryDAO rssCategoryDAO;
#Override
public RssCategory findOrCreate(String title) {
RssCategory category = rssCategoryDAO.findByTitle(title);
if (category == null) {
category = new RssCategory();
category.setTitle(title);
category = rssCategoryDAO.save(category);
}
return category;
}
}
How save many to many?
You probably need to save your RssCategory first, in order to have an ID to store in feed_category table. This last save will be automatically made when you make the assignment:
rssFeed.getRssCategories().add(rssCategory);
but first you need to do:
rssFeedService.save(rssCategory);
Probably you'll need to put this operations within a transaction.