I have two entities which are linked via a OneToMany relationship:
#Entity
#Table(name="bookcase")
public class BookCase {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#Transient
#Getter #Setter private Long oldId;
/*
https://vladmihalcea.com/a-beginners-guide-to-jpa-and-hibernate-cascade-types/
*/
#OneToMany(mappedBy = "bookCase", cascade = CascadeType.ALL, orphanRemoval = true)
private Set<Bookshelf> bookShelves = new HashSet<>();
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public Set<Bookshelf> getBookShelves() { return bookShelves; }
public void setBookShelves(Set<Bookshelf> bookShelves) { this.bookShelves = bookShelves; }
}
#Entity
#Table(name="bookshelf")
public class Bookshelf {
private static final Logger log = LoggerFactory.getLogger(Bookshelf.class);
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#Transient
#Getter #Setter private Long oldId;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "bookcase_id")
private BookCase bookCase;
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public BookCase getBookCase() { return bookCase; }
public void setBookCase(BookCase bookCase) {
this.bookCase = bookCase;
bookCase.getBookShelves().add(this);
}
#Transient
#Setter private OldIdListener oldIdListener;
/*
When the id is saved, listening DTOs can update their ids
*/
#PostPersist
public void triggerOldId() {
log.info("Postpersist triggered for {}", id);
if (oldIdListener != null) {
oldIdListener.updateId(oldId, id);
}
}
}
public interface OldIdListener {
void updateId(long oldId, long newId);
}
The following test fails:
#Test
public void testThatCascadingListenerIsTriggered() {
var mock = mock(OldIdListener.class);
var mock2 = mock(OldIdListener.class);
var mock3 = mock(OldIdListener.class);
var bookcase = new BookCase();
var shelf1 = new Bookshelf();
shelf1.setOldId(-5L);
shelf1.setBookCase(bookcase);
shelf1.setOldIdListener(mock);
var shelf2 = new Bookshelf();
shelf2.setOldId(-6L);
shelf2.setBookCase(bookcase);
shelf2.setOldIdListener(mock2);
var saved = bookCaseRepository.save(bookcase);
verify(mock).updateId(eq(-5L), anyLong());
verify(mock2).updateId(eq(-6L), anyLong());
var savedBookCase = bookCaseRepository.findById(saved.getId()).get();
assertThat(savedBookCase.getBookShelves()).hasSize(2);
var shelf3 = new Bookshelf();
shelf3.setOldId(-10L);
shelf3.setBookCase(savedBookCase);
shelf3.setOldIdListener(mock3);
savedBookCase.getBookShelves().add(shelf3);
bookCaseRepository.save(savedBookCase);
verify(mock3).updateId(eq(-10L), anyLong());
}
mock3 is never called.
When debugging the code, I can see that the transient fields oldId and oldIdListener are set to null when the #PostPersist method is called on object shelf3, not on shelf1 and 2.
I think this is because I am modifying the Set object; but the object is correctly persisted, it just loses all transient fields. This does not happen when the entire tree is persisted for the first time.
Is this the wrong way to insert a new element to a OneToMany set or where is the error here?
I'm using Spring Boot 2.1.
Thanks!
The field which annotation with #Transient will not persist to the database, so if you want it to persist, you must remove #Transient.
Related
Getting errors while trying to persist child entity (MsgRetry) when trying to get an entity of parent entity (Msg) where the parent PK (msg_id) is the FK in the child entity.
Errors like: org.hibernate.id.IdentifierGenerationException: attempted to assign id from null one-to-one property
The parent entity, does not need to know about the child entity (at least i don't think it needs to, to work). Once the child entity is persisted I'm trying to also persist the parent entity. I can work around this by not having the parent entity in the child entity and the call the associated repositories. However, I don't think it's as clean as what I'm attempting but obviously more difficult/ complex.
Thanks for any advice on best practices or how to achieve this if this is a good solution.
tables:
msg
msg_id
pk
msg_status
msg_status
msg_retry
msg_id
fk
count
timestamp
model:
#Entity
#Table(name="msg")
public class Msg {
#Id
#Column(name = "msg_id")
#GeneratedValue(strategy = generationtype.sequence, generator = "msg_id_seq_gen")
#SequenceGenerator(name = "msg_id_seq_gen", sequencename = "msg_id_seq", allocationsize = 1)
private Long msgId;
#Column(name = "msg_status", nullable = false)
private String msgStatus;
...
//getters setters
}
#Entity
#Table(name = "msg_retry")
public class MsgRetry implements Serializable{
private static final long serialVersionUID = -7637385223556379976L;
#Id
#Column(name = "msg_id")
private Long msgId;
#OneToOne
#JoinColumn(name="msg_id", referencedColumnName = "msg_id")
private Msg msg;
#Column(name = "count")
private Long count;
#Generated(value = GenerationTime.ALWAYS)
#Column(name = "timestamp")
private Date timestamp;
public MsgRetry() {
}
public MsgRetry(Msg msg, Long count) {
this.msg = msg;
this.count = count;
}
public MsgRetry(Long msgId, Long count) {
this.msgId = msgId;
this.count = count;
}
public Msg getMsg() {
return msg;
}
public void setMsg(Msg msg) {
this.msg = msg;
}
#Repository
public interface MsgRetryRepository extends JpaRepository<MsgRetry, Long>{
}
#Test
public void testSaveMsgByMsgIdRetry() {
msgRetryRepository.deleteAll();
List<Msg> msgs = msgRepository.findAll();
MsgRetry msgRetry = new MsgRetry(msgs.get(0).getMsgId(), 1L);
msgRetry = msgRetryRepository.save(msgRetry);
assertNotNull(msgRetry.getMsg()); // fails to load Msg entity
LOG.info("msgRetry: {}", msgRetry);
}
#Test
public void testSaveMsgRetryByMsg() {
msgRetryRepository.deleteAll();
List<Msg> msgs = msgRepository.findAll();
MsgRetry msgRetry = new MsgRetry(msgs.get(0), 1L);
msgRetry = msgRetryRepository.save(msgRetry);
assertNotNull(msgRetry.getMsg());
LOG.info("msgRetry: {}", msgRetry);
}
Errors out: org.springframework.orm.jpa.JpaSystemException: ids for this class must be manually assigned before calling save(): msgtest.MsgRetry;
First, straighten out your IDs for MsgRetry. The FK should be good enough.
#Entity
#Table(name = "msg_retry")
public class MsgRetry {
#Id
#ManyToOne(optional = false)
#JoinColumn(name = "msg_id")
private Msg msg;
#Column(name = "count")
private Long count;
#Generated(value = GenerationTime.ALWAYS)
#Column(name = "timestamp")
private Date timestamp;
public Msg getMsg() { return msg; }
public void setMsg(Msg msg) { this.msg = msg; }
}
Next, be sure MsgRetryRepository is properly sub-classed:
public interface MsgRetryRepository extends CrudRepository<MsgRetry, Msg>
{
// Empty for now
}
Lastly, query the MsgRetry in the right way:
public class BizLogic {
MsgRetryRepository retryRepo
public MsgRetry retry(Msg msg, String msgStatus) {
MsgRetry mr = retryRepo.findById(msg);
if (mr !=null) {
// XXX I cannot tell from your logic what you are doing here.
retryRepo.save(mr);
}
}
}
What is your persistence layer? Maybe you can turn on debugging and visit the logs to see what is happening under the hood.
Guys hope you doing well under the lockdown?
I have an issue occur in my work project, the problem is Hibernate won't update my child entity when I update it from the View.
my code parent class has:
#OneToMany(cascade = CascadeType.ALL, mappedBy = "permitType", fetch = FetchType.EAGER)
private Set<BlockDate> blockDates;
.....
public Set<BlockDate> getBlockDates() {return blockDates;}
public void setBlockDates(Set<BlockDate> blockDates) {this.blockDates = blockDates;}
My child class has :
#Entity
#Table(name="block_date")
public class BlockDate implements GenericModel<Long> {
#Id
#GeneratedValue(strategy = GenerationType.TABLE, generator = "block_date")
#TableGenerator(name = "block_date", allocationSize = 30, initialValue = 10000)
private Long id;
#Column(name = "effective_date", nullable = false)
private DateTime effectiveDate;
#ManyToOne
#JoinColumn(name = "permit_type_fk")
private PermitType permitType;
#Override
public Long getId() {return id;}
public void setId(Long id) {this.id = id;}
#JsonSerialize(using = CustomDateTimeSerializer.class)
public DateTime getEffectiveDate() {return effectiveDate;}
public void setEffectiveDate(DateTime effectiveDate) {this.effectiveDate = effectiveDate;}
public PermitType getPermitType() {return permitType;}
public void setPermitType(PermitType permitType) {this.permitType = permitType;}
}
My DTO code to control the child entity and pull the entry from the view is:
if (getBlockDates() != null ) {
Set<BlockDate> blockDates = new HashSet<>();
DateTime effectiveDate;
DateTimeZone timeZone = DateTimeZone.forID(permitType.getPermitArea().getOrganisation().getTimezone());
for (String blockDateString : getBlockDates()){
effectiveDate = DATEPICKER_FORMATTER.parseDateTime(blockDateString).withZone(timeZone).withTimeAtStartOfDay();
blockDates.add(getBlockDateAtt(permitType, effectiveDate));
System.out.println("test dates " + blockDateString); //this is testing how many entry from user
}
permitType.setBlockDates(blockDates);
System.out.println("test the new child entity " + permitType.getBlockDates().size()); //it sets the new record on child entity if i print to consol but it wont applies on the DB
} else{
permitType.setBlockDates(new HashSet<>());
System.out.println("test null " + permitType.getBlockDates().size()); // this show 0 but in the DB wont updated the child, it still have the old record
}
and the controller simply have:
permitAreaDao.update(permitArea);
I am trying to add ManyToMany entity to my application. I created entity but cannot implement it.
Actor class
#Entity
#Table(name = "actor")
public class Actor {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
#Column(nullable = false, name = "actor_name")
private String actorName;
#ManyToMany(mappedBy = "actor", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
private Set<Movie> movie = new HashSet<Movie>();
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getActorName() { return actorName; }
public void setActorName(String actorName) {
this.actorName = actorName;
}
public Set<Movie> getMovie() {
return movie;
}
public void setMovie(Set<Movie> movie) {
this.movie = movie;
}
}
In movie class I have
#ManyToMany(cascade = CascadeType.ALL)
#JoinTable(
name = "movie_actor",
joinColumns = {#JoinColumn(name = "movie_id")},
inverseJoinColumns = {#JoinColumn(name = "actor_id")}
)
Set<Actor> actor = new HashSet<Actor>();
........................
public Set<Actor> getActor () {
return actor;
}
public void setActor(Set<Actor> actor){
this.actor = actor;
}
I created my entity just like this but in MovieService;
Actor actor = ActorRepository.findByActorName(movie.getActor().getActorName());
movie.setActor(actor);
This part gives me error. movie.getActor().getActorName() method cannot find. Where do I need to look? In IDE it also says method getActorName and setActorName is never used. I am also adding my ActorRepository and ActorService to closer look to the problem.
ActorRepository
public interface ActorRepository extends JpaRepository<Actor, Integer> {
Set<Actor> findByActorName(String actorName);
}
ActorService
#Service
public class ActorService {
private ActorRepository actorRepository;
#Autowired
public ActorService(ActorRepository actorRepository) {
this.actorRepository = actorRepository;
}
public List<Actor> getAllActor() {
return actorRepository.findAll();
}
}
After adding ManyToMany I was using is as OneToMany entity. Services is works for OneToMany. How can I use them for ManyToMany? I need to add multiple actors to my movies. I couldn't find MVC projects for ManyToMany implementation.
You're invoking movie.getActor().getActorName() which basically does a getActorName() on a Set<Actor> object.
You're basically treating the relation as a ManyToOne instead of a OneToMany
You could use the following to fetch the first Actor of the Set
ActorRepository.findByActorName(movie.getActors().iterator().next().getActorName());
But then of course, you don't have all your Actor's names
What you could do is the following
public interface ActorRepository extends JpaRepository<Actor, Integer> {
Set<Actor> findByActorNameIn(List<String> actorName);
}
And invoke it that way
ActorRepository.findByActorNameIn(
movie.getActors()
.stream()
.map(Actor::getName)
.collect(Collectors.toList())
);
Sales Order Entity.
#Entity
#Table(name = "sales_orders")
#IdClass(ReceiptPK.class)
public class SalesOrders implements Serializable {
public SalesOrders() {
}
#Id
protected Integer receiptID;
#Id
protected Integer dateKey;
public SalesOrders(Integer receiptID, Integer dateKey) {
this.receiptID = receiptID;
this.dateKey = dateKey;
}
//order contains many details
#OneToMany(fetch = FetchType.LAZY, mappedBy = "salesOrders")
#Cascade({CascadeType.SAVE_UPDATE, CascadeType.DELETE})
private Set<SalesOrderDetails> orderDetails = new HashSet<SalesOrderDetails>();
public Set<SalesOrderDetails> getOrderDetails() {
return orderDetails;
}
public void setOrderDetails(Set<SalesOrderDetails> orderDetails) {
this.orderDetails = orderDetails;
}
// other property ..
Order Details Entity.
#Entity
#Table(name = "sales_order_details")
public class SalesOrderDetails implements Serializable {
public SalesOrderDetails() {
}
private int id;
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
// Order holder
private SalesOrders salesOrders;
#ManyToOne
#JoinColumns({
#JoinColumn(name = "order_num", referencedColumnName = "receiptID"),
#JoinColumn(name = "date_key", referencedColumnName = "dateKey")})
public SalesOrders getSalesOrders() {
return salesOrders;
}
public void setSalesOrders(SalesOrders salesOrders) {
this.salesOrders = salesOrders;
}
// other property ...
My Question : When i try to remove Order item that doesn't affect on sales_order_details .
SalesOrders saleOrder = (SalesOrders) getSession().get(SalesOrders.class ,new ReceiptPK(receiptID,dateKey));
saleOrder.getOrderDetails().remove(someDetails);
getSession().beginTransaction();
getSession().saveOrUpdate(saleOrder);
getSession().getTransaction().commit();
But someDetails doesn't removed.
-- Any help will be appreciated ...
I think that you have to commit the same transaction.
getSession().getTransaction().begin();
getSession().saveOrUpdate(saleOrder);
getSession().getTransaction().commit();
For bi-directional associations you should(must) always ensure that the associations are set correctly.
Do not allow direct access to your collections.
Provide add and remove methods for modification.
public Set<SalesOrderDetails> getOrderDetails() {
//force to use add/remove to ensure consistent object model
return Collections.unmodifiableSet(orderDetails);
}
public void addOrderDetails(SalesOrderDetails salesOrderDetails){
orderDetails.add(salesOrderDetails);
salesOrderDetails.setSalesOrders(this); //important
}
public void removeOrderDetails(SalesOrderDetails salesOrderDetails){
orderDetails.remove(salesOrderDetails);
salesOrderDetails.setSalesOrder(null); //important
}
Additionally, have you implemented equals() and hashCode() on your Entitites i.e. when you call salesOrder.getOrderDetails().remove(someDetails) is anything actually being removed from the collection?
Probably not if you have not implemented equals() and hashCode() on SalesOrderDetails.
Fianlly, you shout set the orphanRemoval flag on the OneToMany mapping to true:
http://en.wikibooks.org/wiki/Java_Persistence/Relationships#Orphan_Removal_.28JPA_2.0.29
I'm currently working on system that consists of Java Web app and C# client app. Web app has Java Web Service, which has method that returns entity object of Program class:
#WebMethod(operationName = "getProgram")
public Program getProgram(#WebParam(name = "macAddress") String macAddress){
Device device = DeviceManager.getInstance().getDevice(macAddress);
if(device != null){
return device.getProgram();
}
return null;
}
This return object of type Program which has many properties and relations:
#Entity
#Table(name = "PROGRAM", schema = "APP")
#XmlRootElement
#NamedQueries({
#NamedQuery(name = "Program.getProgramsByWeather", query = "SELECT p FROM Program p WHERE p.weather = :weather")})
public class Program extends DbEntity implements Serializable {
private static final long serialVersionUID = 1L;
#JoinColumn(name = "LOGO_ID", referencedColumnName = "ID")
#OneToOne(cascade = CascadeType.ALL, orphanRemoval = true, fetch= FetchType.EAGER)
private Logo logo;
#JoinColumn(name = "WEATHER_ID", referencedColumnName = "ID")
#ManyToOne
private Weather weather;
#OneToMany(cascade = CascadeType.ALL, mappedBy = "program", orphanRemoval = true)
private List<ProgramPlaylist> programPlaylistList = new ArrayList<>();
#OneToMany(cascade = CascadeType.ALL, mappedBy = "program", orphanRemoval = true)
private List<ProgramTicker> programTickerList = new ArrayList<>();
#Column(name = "UPDATED")
private boolean updated;
public Program() {
}
public Program(String name, AppUser owner) {
super(name, owner);
}
public Logo getLogo() {
return logo;
}
public void setLogo(Logo logo) {
this.logo = logo;
}
public Weather getWeather() {
return weather;
}
public void setWeather(Weather weather) {
this.weather = weather;
}
public boolean isUpdated() {
return updated;
}
public void setUpdated(boolean updated) {
this.updated = updated;
}
#XmlElement
public List<ProgramPlaylist> getProgramPlaylistList() {
return programPlaylistList;
}
#XmlElement
public List<ProgramTicker> getProgramTickerList() {
return programTickerList;
}
#Override
public String toString() {
return "Program[ id=" + getId() + " ]";
}
}
Client can get this object and accessing some properties in client app like program.name, which it inherits from DbEntity, but when i try to call something like this:
program.logo.name
client throws NullReferenceException.
Same exception occurs when i try to iterate over the elements of programPlaylistList ArrayList.
I'm assuming that the object itself that is passed through to client isn't fully loaded.
How can i solve this problem, please help?!
EDIT
Ok, so I printed out XML response that client get from service and its populated correctly, but for some reason object fields aren't populated and are mostly null.
Why is this occurring?
Bye default, the fetch strategy for #OneToMany annotations is LAZY, have you tried specifying it to EAGER like in the #oneToOne field (fetch= FetchType.EAGER)?