Why lazy init entity is null when assigned to DTO inside transaction boundary? - spring

I am porting my legacy code to spring boot + angular application, which was spring + angular js earlier. My pojo has #ManyToOne relationship in it which is lazy.
When i try to create a DTO object from original object, the child object inside orginal object is null, when sent to client. In my legacy application same code works perfectly.
If i make it eager or i call sysout on that child element before creating DTO then it works, probably because getters of child object is called internally.
Parent object
public class ComponentInfo implements Serializable{
private static final long serialVersionUID = -1135710750835719391L;
#Id
#Column(name="TAGGING_KEY")
private String taggingKey;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "COMPONENT_CONFIG_ID",nullable = true)
private ReportComponentConfig componentConfig;
#Column(name = "DESCRIPTION", length = 200)
private String description;
#Column(name = "ORIGINAL_FILE_NAME",length=50)
private String originalFileName;
#Column(name = "OVERRIDE_DOCUMENT")
private Boolean overrideDocument = false;
#Column(name = "START_DATE")
private Date startDate;
#OneToMany(mappedBy="componentInfo",cascade=CascadeType.ALL,orphanRemoval=true)
private List<TransactionComponentInfo> transactionComponentInfo = new ArrayList<>(0);
#Column(name = "SHOW_GLOBAL")
private Boolean showGlobal = false;
}
Child Object
public class ReportComponentConfig {
#Id
#TableGenerator(name = "COMPONENT_CONFIG_ID", table = "ID_GENERATOR", pkColumnName = "GEN_KEY", valueColumnName = "GEN_VALUE", pkColumnValue = "COMPONENT_CONFIG_ID", allocationSize = 1, initialValue = 1)
#GeneratedValue(strategy = GenerationType.TABLE, generator = "COMPONENT_CONFIG_ID")
#Column(name = "COMPONENT_CONFIG_ID", unique = true, nullable = false)
private int id;
#Column(name = "NAME", nullable = false, length = 200)
private String name;
#Column(name = "TAG", nullable = false, length = 200)
private String tag;
#Column(name = "COMP_CONFIG", nullable = false, columnDefinition = "VARCHAR(MAX)")
private String config;
#Column(name = "PUBLISHED_CONFIG", columnDefinition = "VARCHAR(MAX)")
private String publishedConfig;
#Column(name = "IS_PUBLISHED")
private boolean published = false;
#ManyToOne
#JoinColumn(name = "COMPONENT_ID", nullable = false)
private Component component;
#ElementCollection(fetch = FetchType.EAGER)
#CollectionTable(name = "CONFIG_REPORT_MAPPING", joinColumns = #JoinColumn(name = "COMPONENT_CONFIG_ID"))
#Column(name = "REPORT_ID")
private Set<Long> reports = new HashSet<>(0);
#ElementCollection(fetch = FetchType.LAZY)
#CollectionTable(name = "CONFIG_TRANSACTION_MAPPING", joinColumns = #JoinColumn(name = "COMPONENT_CONFIG_ID"))
#Column(name = "TRANSACTION_ID")
private Set<Long> transactions = new HashSet<>(0);
#Column(name = "VIEWS", columnDefinition = "VARCHAR(MAX)")
private String views;
}
DTO
public class ComponentInfoDTO implements Cloneable {
private ReportComponentConfig componentConfig;
private String taggingKey;
private String description;
private String originalFileName;
private Date startDate;
private Boolean overrideDocument;
private Boolean showGlobal;
private ComponentInfoDTO parentComponentInfo;
public ComponentInfoDTO() {
}
public ComponentInfoDTO(ComponentInfo ci, TransactionComponentInfo transactionComponentInfo) {
this.componentConfig = ci.getComponentConfig();//this is object is null
this.taggingKey = ci.getTaggingKey();
this.description = ci.getDescription();
this.originalFileName = ci.getOriginalFileName();
this.startDate = ci.getStartDate();
this.overrideDocument = ci.getOverrideDocument();
this.showGlobal = ci.getShowGlobal();
if (transactionComponentInfo != null) {
this.parentComponentInfo = this.clone();
this.startDate = transactionComponentInfo.getStartDate();
this.overrideDocument = transactionComponentInfo.getOverrideDocument();
this.description = transactionComponentInfo.getDescription();
this.originalFileName = transactionComponentInfo.getOriginalFileName();
this.showGlobal = true;
}
}
}
New Code image
Old Code image
Edit:
Both are same but in older case i get the child object on client side and in new case i get null.
This is the data i'm getting in my older application with lazy init
[ {
"componentConfig" : {
"id" : 3,
"name" : "Monthly Origination By Region",
"tag" : "CHART_monthlyOriginationByRegion",
"config" : "xyz",
"published" : true,
"component" : {
"id" : "CHART",
"name" : "Chart",
"defaultConfig" : null,
"htmlTag" : "<chart></chart>",
"filePath" : "chart/chart.component.js",
"dependentScriptsSrc" : [ ],
"dependencies" : null
},
"reports" : [ 3 ],
"transactions" : [ 2 ],
"views" : "{\"monthlyOriginationByRegion\": {\"key\": \"MONTHLY_ORIGINATION_BY_REGION\"}}"
},
"taggingKey" : "3",
"description" : "asdfasd\nasdfadf",
"originalFileName" : "Citi Tape - 2141 - GEBL0501 - 2019 Oct 04.xlsm",
"startDate" : "2019-10-28T18:30:00.000+0000",
"overrideDocument" : true,
"showGlobal" : true,
"parentComponentInfo" : null
} ]
This is the data in new application
[ {
"componentConfig" : null,
"taggingKey" : "3",
"description" : "asdfasd\nasdfadf",
"originalFileName" : "Citi Tape - 2141 - GEBL0501 - 2019 Oct 04.xlsm",
"startDate" : "2019-10-28T18:30:00.000+0000",
"overrideDocument" : true,
"showGlobal" : true,
"parentComponentInfo" : null
} ]
Component config should not be null, if make it eager fetch it works in new app but in my older application, it is working with lazy fetch.

What you see in the debugger window is a HibernateProxy. The fields of that proxy are never initialized! The getters are intercepted and delegated to the loaded entity which is located somewhere inside the proxy.
This means you have to never call fields directly, as you don’t know if the object is a proxy or your loaded entity. You always need to work on the getters.
How are you mapping the entity to the DTO? Most likely you use field access, while you should use the getters.

Related

#JoinColumn "occurs out of order" when upgrading to spring-boot-3 (Hibernate 6 )

I have the following usage in JoinColumns
#Entity
public class EntityOne{
private String action;
private String type;
#ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.PERSIST)
#NotFound(action = NotFoundAction.IGNORE)
#JoinColumns({
#JoinColumn(name = "action", referencedColumnName = "action_name", updatable = false, insertable = false),
#JoinColumn(name = "type", referencedColumnName = "type_name", updatable = false, insertable = false)
})
private Entitytwo entitytwo;
}
And
#Entity
public class EntityTwo {
#Id
#Column(name = "type_name")
private String typeName;
#Id
#Column(name = "action_name")
private String actionName;
}
This setup causes hibernate error of
Referenced column '" + column.getName()
+ "' mapped by target property '" + property.getName()
+ "' occurs out of order in the list of '#JoinColumn's
If i change the order inside the #JoinColumns it seems to work, but can stop working at the next time the application starts.
The hibernate comments at the begining of the relevant code states:
// Now we need to line up the properties with the columns in the
// same order they were specified by the #JoinColumn annotations
// this is very tricky because a single property might span
// multiple columns.
// TODO: For now we only consider the first property that matched
// each column, but this means we will reject some mappings
// that could be made to work for a different choice of
// properties (it's also not very deterministic)
And on the relevant code itself:
// we have the first column of a new property
orderedProperties.add( property );
if ( property.getColumnSpan() > 1 ) {
if ( !property.getColumns().get(0).equals( column ) ) {
// the columns have to occur in the right order in the property
throw new AnnotationException("Referenced column '" + column.getName()
+ "' mapped by target property '" + property.getName()
+ "' occurs out of order in the list of '#JoinColumn's");
}
currentProperty = property;
lastPropertyColumnIndex = 1;
}
How should i set the #JoinColumn for it to consistently work?
If the action and type attributes of EntityOne are meant to refer to the corresponding attributes of EntityTwo, they are useless and misleading.
The attribute private Entitytwo entitytwo is enough to design the #ManytoOne relation.
Remove these two attributes and if you need to get the action and type value of the entityTwo linked to an entityOne, simply use entityOne.entitytwo.getAction() (or entityOne.entitytwo.getType()).
I just tried the code you posted in Hibernate 6.1, and I observed no error. Even after permuting various things, still no error. So then to make things harder, I added a third column to the FK and tried permuting things. Still no error.
I now have:
#Entity
public class EntityOne {
#Id #GeneratedValue
Long id;
String action;
String type;
int count;
#ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.PERSIST)
#NotFound(action = NotFoundAction.IGNORE)
#JoinColumns({
#JoinColumn(name = "count", referencedColumnName = "count", updatable = false, insertable = false),
#JoinColumn(name = "action", referencedColumnName = "action_name", updatable = false, insertable = false),
#JoinColumn(name = "type", referencedColumnName = "type_name", updatable = false, insertable = false),
})
EntityTwo entitytwo;
}
#Entity
public class EntityTwo {
#Id
#Column(name = "type_name")
String typeName;
#Id
#Column(name = "count")
int count;
#Id
#Column(name = "action_name")
String actionName;
}
and the test code:
#DomainModel(annotatedClasses = {EntityOne.class, EntityTwo.class})
#SessionFactory
public class BugTest {
#Test
public void test(SessionFactoryScope scope) {
scope.inTransaction( session -> {
EntityOne entityOne = new EntityOne();
entityOne.action = "go";
entityOne.type = "thing";
EntityTwo entityTwo = new EntityTwo();
entityTwo.actionName = "go";
entityTwo.typeName = "thing";
entityOne.entitytwo = entityTwo;
session.persist( entityOne );
} );
}
}
Perhaps there's something you're not telling us? Like, for example, something to do with the #Id of EntityOne which is missing in your original posted code?
Just in case, also tried this variation:
#Entity
public class EntityOne {
#Id
String action;
#Id
String type;
#Id
int count;
#ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.PERSIST)
#NotFound(action = NotFoundAction.IGNORE)
#JoinColumns({
#JoinColumn(name = "action", referencedColumnName = "action_name", updatable = false, insertable = false),
#JoinColumn(name = "count", referencedColumnName = "count", updatable = false, insertable = false),
#JoinColumn(name = "type", referencedColumnName = "type_name", updatable = false, insertable = false),
})
EntityTwo entitytwo;
}
But still no error.

how to specify List of entites in an entity using JPA

I have two entities. A vulnerability can have multiple vulnerability identifiers.
#Entity
#JsonInclude(Include.NON_NULL)
#ApiModel(parent = ApprovableEntity.class)
public class Vulnerability {
...
#JsonProperty("vulnerabilityIdentifiers")
#JoinColumn(name = "vulnerabilityidentifier_id")
#JsonView(JsonViews.BasicChangeLogView.class)
#OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
private List<VulnerabilityIdentifier> vulnerabilityIdentifiers;
...
}
#Entity
#ApiModel(parent = ApprovableEntity.class)
public class VulnerabilityIdentifier {
...
#ManyToOne
#JoinColumn(name = "vulnerability_id", referencedColumnName = "id")
#NotNull(message = "vulnerability is required")
#JsonView({JsonViews.BasicApprovableView.class, JsonViews.BasicChangeLogView.class,
JsonViews.ChangeLogAnswerView.class, JsonViews.DraftAnswerView.class})
#ApiModelProperty(hidden = true)
private Vulnerability vulnerability;
#Column(name = "type")
#JsonProperty("type")
#Size(max = 12)
#NotNull(message = "CVEID type required")
#ApiModelProperty(accessMode = ApiModelProperty.AccessMode.READ_ONLY)
private String cveIdType;
#Column(name = "value")
#JsonProperty("value")
#Size(max = 24)
#NotNull(message = "value is required")
#ApiModelProperty(accessMode = ApiModelProperty.AccessMode.READ_ONLY)
private String value;
...
}
Now when I send in a json request to the endpoint like as under, the application throws exception that it cannot map the type and value fields in the vulnerabilityIdentifier field.
A sample json request
{
"vulnerabilityImpacts": {
},
"vulnerabilityIdentifiers": [{"type": "cveId", "value": "CVE-1234-12345"}],
"vulnerableThreeppcomponents": [],
"internalSource": "**",
"cveId": "*****",
......
}
Both the cveId and value properties are annotated with #ApiModelProperty(accessMode = ApiModelProperty.AccessMode.READ_ONLY) which means they are ignored when deserializing. Remove this annotation from both properties.

Repository GET returns JSON which contains both a content object and content array

I have an entity with a ManyToOne relationship, when I use restTemplate.getForEntity(), the nested entity has its values wrapped in a content field at the same time there is a content array added overriding it
#ManyToOne(fetch = FetchType.LAZY)
#RestResource(exported = false)
#JoinColumn(name = "namespace", nullable = false)
private Namespace namespace;
A GET on the entity with this relation returns the following output body
{
"id" : "some_containing_id",
"alertDefinition" : null,
"namespace" : {
"content" : {
"id" : "some_namespace_id",
"emailSenderName" : "some sender",
"emailSenderId" : "foo#bar.com",
"createdAt" : "2018-07-19T05:24:04.473Z",
"updatedAt" : "2018-07-19T05:24:04.473Z"
},
"content" : [ ],
"links" : [ ]
},
...
So the namespace is being serialized containing 2 content fields with the array replacing the content object containing the values
SpringBoot 2.0.3.RELEASE
Namespace.java
#Entity
#Table(name = "namespace"
, schema = "alert_notification"
)
public class Namespace implements java.io.Serializable {
public transient static final String
EMAIL_SENDER_NAME_DEFAULT = "some sender";
public transient static final String
EMAIL_SENDER_ID_DEFAULT = "foo#vbar.com";
#Id
#Column(name = "id", unique = true, nullable = false)
private String id;
#Builder.Default
#Column(name = "email_sender_name")
private String emailSenderName = EMAIL_SENDER_NAME_DEFAULT;
#Builder.Default
#Column(name = "email_sender_id")
private String emailSenderId = EMAIL_SENDER_ID_DEFAULT;
#CreationTimestamp
#Column(name = "created_at", nullable = false, updatable = false)
private OffsetDateTime createdAt;
#UpdateTimestamp
#Column(name = "updated_at", nullable = false)
private OffsetDateTime updatedAt;
}
For some reason the issue is caused by the Hibernate5 module included in com.fasterxml.jackson.datatype:jackson-datatype-hibernate5 which was recently introduced.
Interestingly enough the module doesn't even need to be enabled, having the dependency on the classpath alone will cause the problem.

Jpa - Hibernate ManyToMany do many insert into join table

I have follows ManyToMany relationship between WorkDay(has annotation ManyToMany) and Event
WorkDay entity
#Entity
#Table(name = "WORK_DAY", uniqueConstraints = { #UniqueConstraint(columnNames = { "WORKER_ID", "DAY_ID" }) })
#NamedQueries({
#NamedQuery(name = WorkDay.GET_WORK_DAYS_BY_MONTH, query = "select wt from WorkDay wt where wt.worker = :worker and to_char(wt.day.day, 'yyyyMM') = :month) order by wt.day"),
#NamedQuery(name = WorkDay.GET_WORK_DAY, query = "select wt from WorkDay wt where wt.worker = :worker and wt.day = :day") })
public class WorkDay extends SuperClass {
private static final long serialVersionUID = 1L;
public static final String GET_WORK_DAYS_BY_MONTH = "WorkTimeDAO.getWorkDaysByMonth";
public static final String GET_WORK_DAY = "WorkTimeDAO.getWorkDay";
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "WORKER_ID", nullable = false)
private Worker worker;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "DAY_ID", nullable = false)
private Day day;
#Column(name = "COMING_TIME")
#Convert(converter = LocalDateTimeAttributeConverter.class)
private LocalDateTime comingTime;
#Column(name = "OUT_TIME")
#Convert(converter = LocalDateTimeAttributeConverter.class)
private LocalDateTime outTime;
#Enumerated(EnumType.STRING)
#Column(name = "STATE", length = 16, nullable = false)
private WorkDayState state = WorkDayState.NO_WORK;
#ManyToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
#JoinTable(name = "WORK_DAY_EVENT", joinColumns = {
#JoinColumn(name = "WORK_DAY_ID", nullable = false)}, inverseJoinColumns = {
#JoinColumn(name = "EVENT_ID", nullable = false)})
#OrderBy(value = "startTime desc")
private List<Event> events = new ArrayList<>();
protected WorkDay() {
}
public WorkDay(Worker worker, Day day) {
this.worker = worker;
this.day = day;
this.state = WorkDayState.NO_WORK;
}
}
Event entity
#Entity
#Table(name = "EVENT")
public class Event extends SuperClass {
#Column(name = "DAY", nullable = false)
#Convert(converter = LocalDateAttributeConverter.class)
private LocalDate day;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "TYPE_ID", nullable = false)
private EventType type;
#Column(name = "TITLE", nullable = false, length = 128)
private String title;
#Column(name = "DESCRIPTION", nullable = true, length = 512)
private String description;
#Column(name = "START_TIME", nullable = false)
#Convert(converter = LocalDateTimeAttributeConverter.class)
private LocalDateTime startTime;
#Column(name = "END_TIME", nullable = true)
#Convert(converter = LocalDateTimeAttributeConverter.class)
private LocalDateTime endTime;
#Enumerated(EnumType.STRING)
#Column(name = "STATE", nullable = false, length = 16)
private EventState state;
protected Event() {
}
}
Attached UI form for clarity
When I push Clock with run icon first time, it means "create event and start work day" in bean, calling the following methods:
public void startEvent() {
stopLastActiveEvent();
Event creationEvent = new Event(workDay.getDay().getDay(), selectedEventType, selectedEventType.getTitle(),
LocalDateTime.now());
String addEventMessage = workDay.addEvent(creationEvent);
if (Objects.equals(addEventMessage, "")) {
em.persist(creationEvent);
if (workDay.isNoWork()
&& !creationEvent.getType().getCategory().equals(EventCategory.NOT_INFLUENCE_ON_WORKED_TIME)) {
startWork();
}
em.merge(workDay);
} else {
Notification.warn("Невозможно создать событие", addEventMessage);
}
cleanAfterCreation();
}
public String addEvent(Event additionEvent) {
if (!additionEvent.getType().getCategory().equals(NOT_INFLUENCE_ON_WORKED_TIME)
&& isPossibleTimeBoundaryForEvent(additionEvent.getStartTime(), additionEvent.getEndTime())) {
events.add(additionEvent);
changeTimeBy(additionEvent);
} else {
return "Пересечение временых интервалов у событий";
}
Collections.sort(events, new EventComparator());
return "";
}
private void startWork() {
workDay.setComingTime(workDay.getLastWorkEvent().getStartTime());
workDay.setState(WorkDayState.WORKING);
}
In log I see:
insert into event table
update work_day table
insert into work_day_event table
on UI updated only attached frame. Always looks fine.. current WorkDay object have one element in the events collection, also all data is inserted into DB.. but if this time edit event row
event row listener:
public void onRowEdit(RowEditEvent event) {
Event editableEvent = (Event) event.getObject();
LocalDateTime startTime = fixDate(editableEvent.getStartTime(), editableEvent.getDay());
LocalDateTime endTime = fixDate(editableEvent.getEndTime(), editableEvent.getDay());
if (editableEvent.getState().equals(END) && startTime.isAfter(endTime)) {
Notification.warn("Невозможно сохранить изменения", "Время окончания события больше времени начала");
refreshEvent(editableEvent);
return;
}
if (workDay.isPossibleTimeBoundaryForEvent(startTime, endTime)) {
editableEvent.setStartTime(startTime);
editableEvent.setEndTime(endTime);
workDay.changeTimeBy(editableEvent);
em.merge(workDay);
em.merge(editableEvent);
} else {
refreshEvent(editableEvent);
Notification.warn("Невозможно сохранить изменения", "Пересечение временых интервалов у событий");
}
}
to the work_day_event insert new row with same work_day_id and event_id data. And if edit row else do one more insert and etc.. In the result I have several equals rows in work_day_event table. Why does this happen?
link to github project repository(look ver-1.1.0-many-to-many-problem branch)
Change CascadeType.ALL to CascadeType.MERGE for events in the WorkDay entity
Use this code
#ManyToMany(fetch = FetchType.LAZY, cascade = CascadeType.MERGE)
instead of
#ManyToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
Do not use ArrayList, use HashSet. Because ArrayList allows duplicates.
For more info about CasecadeType, follow the tutorial:
Hibernate JPA Cascade Types
Cascading best practices
I think the simple solution is to remove the cascade on many to many relationship and do the job manually ! . I see you already doing it redundantly anyway . So try removing you CascadeType.ALL
#ManyToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
How to persist #ManyToMany relation - duplicate entry or detached entity

Mapping of oneToMany with composite key using eclipselink gives me ORA-01400

I am trying to map the classic bi-directional oneToMany using eclipselink.
My problem is that when i want to insert a new 'child' i get
SQLIntegrityConstraintViolationException.
The database is described like this :
#Entity
#IdClass(KuponPK.class)
#Table(name = "KUPON", schema = "POST", catalog = "")
public class Kupon implements Serializable {
private Integer id;
private String spil;
private Collection<Kombination> kombinationList;
#OneToMany(mappedBy = "kupon", cascade = CascadeType.PERSIST)
public Collection<Kombination> getKombinationList() {
return kombinationList;
}
public class KuponPK implements Serializable {
private Integer id;
private String spil;
#Id
#Column(name = "ID", nullable = false, insertable = true, updatable = true, precision = 0)
public Integer getId() {
return id;
}
#Id
#Column(name = "SPIL", nullable = false, insertable = true, updatable = true, length = 5)
public String getSpil() {
return spil;
}
#Entity
#Table(name = "KOMBINATION", schema = "POST", catalog = "")
public class Kombination {
private Integer id;
private String sorteringOrden;
private Integer sorteringNr;
private Integer antalSpillede;
private BigDecimal odds;
private Kupon kupon;
#ManyToOne
#JoinColumns({#JoinColumn(name = "KUPON_ID", referencedColumnName = "ID", nullable = false, insertable=false, updatable=false),
#JoinColumn(name = "KUPON_SPIL", referencedColumnName = "SPIL", nullable = false, insertable=false, updatable=false)})
public Kupon getKupon() {
return kupon;
}
In my stateless session i have a Kupon object and i create a new Kombination where i set the Kupon and try to merge, but im getting
Internal Exception: java.sql.SQLIntegrityConstraintViolationException: ORA-01400: cannot insert NULL into ("KOMBINATION"."KUPON_ID")
which is obvious since its part of primary key
I am setting the Kombination to Kupon and the Kupon to Kombination, but that doesnt make any difference
How can can i tell that the key is inside the Kupon object which im setting in the Kombination object ??

Resources