Get Id of the associated object after save - java

There is simple model of associated entities:
MainModel class
#Entity
#Table(name = "main_model")
public class MainModel
{
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
#OneToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
#JoinColumn(name = "id_main_model", referencedColumnName = "id")
private List<AssociatedModel> am_collection;
public MainModel() { }
public long getId()
{
return this.id;
}
public void addAssociated(AssociatedModel am)
{
am_collection.add(am);
}
}
AssociatedModel class
#Entity
#Table(name = "associated_model")
public class AssociatedModel
{
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
public AssociatedModel() { }
public long getId()
{
return this.id;
}
}
Simplest jpa repository for MainModel
public interface MainModelDAO extends JpaRepository<MainModel, Long>
{
}
and finally, controller's method for creating and saving AssociatedModel instance
#RequestMapping("/url")
public String createAssociated(#RequestParam("id_mainmodel") long id_mainmodel, #RequestBody AssociatedModel newAm)
{
MainModel mm = MainModelDAOobject.findOne(id_mainmodel);// MainModelDAOobject - #Autowired
mm.addAssociated(newAm);
MainModelDAOobject.saveAndFlush(mm);
return String.valueOf(newAm.getId());// get Id of associated saved object
}
Associated obect saves into database correctly, but id value of this object, allowed by its getId() method, always zero. How can I get Id of the saved associated object correctly?

Try calling MainModelDAOobject.flush() after saving the value, but before reading the ID.
See this answer for an explanation.
Edit: try manually saving newAm by adding a new line:
mm.addAssociated(newAm); // Existing line
newAm = MainModelDAOobject.save(newAm); // New line
MainModelDAOobject.save(mm); // Existing line

you need a sequencegenerator:
#GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "MainModel SequenceGenerator")
#SequenceGenerator(name = "MainModel SequenceGenerator", sequenceName = "MainModel Sequence", initialValue = SEQUENCE_INITIAL_VALUE, allocationSize = SEQUENCE_ALLOCATION_SIZE)

Thanks to David Levesque for helpful suggestion.
problem is solved as follows:
need additional repository for AssotiatedModel
public interface AssotiatedModelDAO extends JpaRepository {}
then just save associated object manually:
mm.addAssociated(newAm);
newAm = AssotiatedModelDAOobject.save(newAm);
newAm = MainModelDAOobject.save(newAm)`;
MainModelDAOobject.save(mm);

Related

How to use graphql with jpa if schema is different to database structure

There is a given database structure and graphql schema.
Fortunately they have a lot in common but unfortunately there are some difference.
Let's say there are entities in java to match the following database structure.
SQL:
TABLE ANIMAL
+ID NUMBER(19)
+NR_OF_LEGS NUMBER(19)
TABLE SHEEP
+ID NUMBER
+LAST_TIME_SHEARED DATETIME
+ANIMAL_ID NUMBER(19)
TABLE COW
+MILK_IN_L NUMBER(3)
+ANIMAL_ID NUMER(19)
Java:
#Entity
#Table(name = "ANIMAL")
public class Animal
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private long id;
#Column(name="nrOfLegs", nullable=false)
private long nrOfLegs;
}
#Entity
#Table(name = "SHEEP")
public class SheepE
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private long id;
#Column(name="lastTimeSheared", nullable=false)
private Datetime lastTimeSheared;
#ManyToOne(targetEntity = AnimalE.class, cascade = CascadeType.ALL, fetch = FetchType.LAZY, optional = true)
#JoinColumn(name = "animalId", nullable = false, insertable = false, updatable = false)
private Animal animal;
}
#Entity
#Table(name = "COW")
public class CowE
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private long id;
#Column(name="milkInL", nullable=false)
private int milkInL;
#ManyToOne(targetEntity = AnimalE.class, cascade = CascadeType.ALL, fetch = FetchType.LAZY, optional = true)
#JoinColumn(name = "animalId", nullable = false, insertable = false, updatable = false)
private Animal animal;
}
The existing GraphQl schema is considered to be like this:
type Sheep{
id: int!
lastTimeSheard: String!
nrOfLegs: int!
}
type Cow {
id: int!
milkInL: int!
nrOfLegs: int
}
The project uses graphql-java in version 11.0 (guess we should update soon)
<dependency>
<groupId>com.graphql-java</groupId>
<artifactId>graphql-java</artifactId>
<version>11.0</version>
</dependency>
The graphql works fine and isimplemented like this:
#Component
public class GraphQLProvider {
#Autowired
GraphQLDataFetchers graphQLDataFetchers;
private GraphQL graphQL;
#PostConstruct
public void init() {this.graphQL = /*init;*/null;}
private RuntimeWiring buildWiring() {
RuntimeWiring.Builder b = RuntimeWiring.newRuntimeWiring()
.type(TypeRuntimeWiring.newTypeWiring("Query")
.dataFetcher("freightCarrier", graphQLDataFetchers.getCow()))
.type(TypeRuntimeWiring.newTypeWiring("Query")
.dataFetcher("personCarrier", graphQLDataFetchers.getSheep())));
return b.build();
}
}
#Component
public class GraphQLDataFetchers {
#AutoWired
private CowRepository cowRepo;
#AutoWired
private sheepRepository sheepRepo;
public DataFetcher getCow() {
DataFetcher dataFetcher = (DataFetchingEnvironment dfe) -> {
int id = dfe.getArgument("id");
return getGraphQlCowFromCowEntity(cowRepo.getById(id));//dirty!
};
return dataFetcher;
}
public DataFetcher getCow() {
DataFetcher dataFetcher = (DataFetchingEnvironment dfe) -> {
int id = dfe.getArgument("id");
return getGraphQlSheepFromSheepEntity(cowRepo.getById(id));//dirty!
};
return dataFetcher;
}
private Cow getGraphQlCowFromCowEntity(CowE ce){//dirty!
return new Cow(ce.getId(), ce.getMilkInL(),ce.getLegs());
}
private Sheep getGraphQlSheepFromSheepEntity(SheepE se){//dirty!
return new Sheep(se.getId(), se.getLastTime(),se.getLegs());
}
public class Sheep
private long id;
private Datetime lastTimeSheared;
private int nrOfLegs;
public Sheep(long id, DateTime lasttimeSheared, int nrOfLegs){
//u know what happens here
}
}
public class Cow
private long id;
private int milkInL;
private int nrOfLegs;
public Sheep(long id, int milkInL, int nrOfLegs){
//u know what happens here
}
}
So how to get rid of getGraphQlCowFromCowEntity and getGraphQlSheepFromSheepEntity. It double ups the code and also is in direct conflict to what graphql is suppose to be abstraction of the data. With this design here each time all fields are loaded through jpa and not only requested fields.
Imagine this is a way more complex environment with more fields.
The graphql schema can't be changed as it's not my responsibility, changing the entire back-end to match schema is also not what I want to archive.
Kind regards
You should use DTO. Retrieving and sending entity object is bad practice as you do not want your grahql api to change every time you refactor you database model, or in your case. Your Sheep and Cow objects are DTO, but you will need some way to convert your entity to DTO (getGraphQlCowFromCowEntity is fine, but you could use polymorphism - CowEntity.toDTO() - or have a service layer do the conversion, there are plenty of way to do this).
To answer your concerns about loading only the requested data, you want your DTO object to only be populated with the requested fields. One way to do this is, instead of populating all fields, have the DTO own a reference to the entity object and retrieve the data from the entity object only when requested.
public class Sheep {
private SheepE entity;
public Sheep(SheepE entity){
this.entity=entity;
}
public getId() {
return entity.getId();
}
public getLastTimeSheared() {
return entity.getLastTimeSheared();
}
...
}
Please see this answer I wrote to a similar question: Graphql Tools: Map entity type to graphql type

Transient field is lost when appending new OneToMany entity

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.

Hibernate, Extending Entity - "java.lang.IllegalArgumentException: Unknown entity"

I have a Hibernate Entity, BaseEvent, which works fine:
import javax.persistence.*;
#Entity
#Table(name = "base_event")
#SequenceGenerator(name = "seq", allocationSize = 1, sequenceName = "seq")
public class BaseEvent
{
#Id
#GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "seq")
#Column(name = "id")
protected Long id = null;
#Column(name = "my_field", nullable = false)
protected String myField;
public Long getId()
{
return id;
}
public void setId(final Long id)
{
this.id = id;
}
public String getMyField()
{
return myField;
}
public void setMyField(final String myField)
{
this.myField = myField;
}
}
I want to identify when this object is changed and update some Map in my application. The easiest way I could think of doing this was to extend BaseEvent and override the setter:
import java.text.MessageFormat;
public class ExtendedEvent extends BaseEvent
{
#Override
public void setMyField(final String myField)
{
System.out.println(MessageFormat.format("Setting myField to {0}", myField));
super.setMyField(myField);
}
}
This works fine in my application, but then when I come to persist the Entity, Hibernate complains it doesn't know what an ExtendedEvent is.
java.lang.IllegalArgumentException: Unknown entity: my.package.ExtendedEvent
I can see that extending Hibernate Entities is a non-trivial problem, especially when you start adding fields - but all I want is for Hibernate to treat ExtendedEvent as a BaseEvent (because it is). Is there a simple solution for this?
Make base event #MappedSuperclass and extending class #Entity
so
#MappedSuperclass
public class BaseEvent
and
#Entity
#Table(whatever)
public class ExtendedEvent extends BaseEvent
If you want to update your map only when changes are updated in the data store, I would recommend implementing onFlushDirty in a Hibernate Interceptor. This method gets called whenever the Session is flushed to the database for every entity change. You can check the object type in the onFlushDirty method for your entity of interest and property of interest.

#Formula not working in hibernate with object

I have a enum of few status value
NEW, REVIEWD, PUBLISHED, PENDING, UPDATED, SPAM, DUPLICATE, IRRELEVANT, UNPUBLISHED
I don't want to use them as enumerated so created one entity for that. For convenient I want to keep a column in entity to initialize status from enum and convert that enumerated value to a Object of status entity. for this..
I have two entity. I want to refer a column with value from another entity.
Basically I want to initialize a object with formula.
Entities are
#Entity
#Table(name = "event_status")
public class EventStatus {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#Column(name="eventStatusId")
private Integer eventStatusId;
#Enumerated(EnumType.STRING)
#Column(unique = true,name="eventStatusType")
private EventStatusType eventStatusType;
public EventStatus() {
this(EventStatusType.NEW);
}
public EventStatus(EventStatusType eventStatusType) {
super();
this.eventStatusType = eventStatusType;
}
public Integer getEventStatusId() {
return eventStatusId;
}
public EventStatusType getEventStatusType() {
return eventStatusType;
}
public void setEventStatusId(Integer eventStatusId) {
this.eventStatusId = eventStatusId;
}
public void setEventStatusType(EventStatusType eventStatusType) {
this.eventStatusType = eventStatusType;
}
}
I have another entity in which I am referring object of this entity
#Entity
#Table(name = "event_")
#Inheritance(strategy = InheritanceType.JOINED)
public abstract class Event implements Serializable {
private static final long serialVersionUID = 1L;
#Id
#Column(name = "id_")
#GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
#Transient
public EventStatusType eventStatusType = EventStatusType.NEW;
#ManyToOne(fetch = FetchType.EAGER, targetEntity = EventStatus.class)
#Formula("select * from event_status where eventStatusId= 1")
private EventStatus status;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public EventStatus getStatus() {
System.out.println("Event.getStatus() " + status);
return status;
}
public void setStatus(EventStatus status) {
System.out.println("Event.setStatus()");
this.status = status;
}
}
This is not giving any exception but not initializing this value.
Is it possible to initialize this EntityStatus with value of eventStatusType in Event entity
I would like to explain that based on the documentation:
5.1.4.1.5. Formula
Sometimes, you want the Database to do some computation for you rather than in the JVM, you might also create some kind of virtual column. You can use a SQL fragment (aka formula) instead of mapping a property into a column. This kind of property is read only (its value is calculated by your formula fragment).
#Formula("obj_length * obj_height * obj_width")
public long getObjectVolume()
The SQL fragment can be as complex as you want and even include subselects.
...
5.1.7.1. Using a foreign key or an association table
...
Note
You can use a SQL fragment to simulate a physical join column using the #JoinColumnOrFormula / #JoinColumnOrformulas annotations (just like you can use a SQL fragment to simulate a property column via the #Formula annotation).
#Entity
public class Ticket implements Serializable {
#ManyToOne
#JoinColumnOrFormula(formula="(firstname + ' ' + lastname)")
public Person getOwner() {
return person;
}
...
}
Also, we should use insertable = false, updatable = false, because such mapping is not editable

Spring Data Repository Save Not Returning Instance With Updated Audit Fields

Why does repository.save(myEntity) not return an updated entity with the updated audit fields?
The resulting instance from MyEntityRepository.save(myEntity) and subsequently, from MyEntityService.save(myEntity) does not have the updated updatedOn date. I have verified this is correctly set in the database, so I know that auditing is working. The returned instance's updatedOn date is correct for an insert, but not for an update. I prefer to not have to immediately do a findById after every save, especially if the intent is that save() returns the udpated, attached instance.
Assuming the setting of updatedOn is occurring through a #PreUpdate hook and this hook is triggered during the entityManager.merge() call via repository.save(), I don't follow why the value would not be set on the returned instance.
Example code:
#Entity
#DynamicUpdate
#DynamicInsert
#Table(name = "my_entity", schema = "public")
#SequenceGenerator(name = "pk_sequence", sequenceName = "my_entity_seq", allocationSize = 1)
#AttributeOverrides({#AttributeOverride(name = "id", column = #Column(name = "id", columnDefinition = "int"))})
#EntityListeners(AuditingEntityListener.class)
public class MyEntity {
protected Integer id;
#LastModifiedDate
private Date updatedOn;
#Id
#GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "pk_sequence")
#Column(name = "id", nullable = false, columnDefinition = "bigint")
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
#Version
#Temporal(TemporalType.TIMESTAMP)
#Column(name = "updated_on")
public Date getUpdatedOn() {
return updatedOn;
}
public void setUpdatedOn(Date updatedOn) {
this.updatedOn = updatedOn;
}
}
public interface MyEntityRepository extends JpaRepository<MyEntity, Integer> { }
#Service
#Transactional(readOnly = true)
public class MyEntityService {
#Autowired
private MyEntityRepository repository;
#Transactional
public MyEntity save(MyEntity myEntity) {
return repository.save(myEntity);
}
}
I faced with the same issue.
In my case the important items that helped me to solve this problem were:
1) use repository.saveAndFlush(...) method
2) use findAllById() or findByYourOwnQuery() (annotated with #Query).
Overall, my test case looked like this:
UserAccount userAccount = UserAccount.builder().username(username).build();
userAccountRepository.saveAndFlush(userAccount);
final LocalDateTime previousUpdateDate = userAccount.getUpdateDate();
....
List<BigInteger> ids = Arrays.asList(userAccountId);
UserAccount updatedUserAccount = userAccountRepository.findAllById(ids).get(0); // contains updated Audit data fields
...
assertThat(actual.getUpdateDate(), is(greaterThan(previousUpdateDate))); // true
The important thing that you shouldn't use repository.findOne(...) because it caches the reference to the object - read more.
I ran in to the exact same problem. I fixed it by using,
repository.saveAndFlush(myEntity);
instead of
repository.save(myEntity);

Categories

Resources