JPA Spring ignores Lazy loading inside #Transacational - java

I have a spring service class where I'm loading a JPA object (target) via CRUD. This target class has a one-to-may mapping that is set to lazy loading.
I would like to query this object inside a spring service method that is annotated with #Transactional and avoid that the childs are being loaded.
When I execute the following code all child data is loaded and laziness is ignored.
#Override
#Transactional
public boolean changeState(boolean enabled, final EventType eventType, final String deviceSerialNumber) {
final UniqueUser user = userService.getUser();
final Target target = targetRepository.findEventsByUserIdAndTenantIdAndTargetDeviceId(user.getUserId(), user.getTenantId(), deviceSerialNumber);
//here everything gets loaded
if (target == null) {
return false;
}
final List<EventHistory> events = target.getEvents().stream()
.filter(event -> event.getEventType() == eventType)
.filter(event -> event.isActive() != enabled)
.collect(Collectors.toList());
events.forEach(event -> event.setActive(enabled));
if (events.isEmpty()) {
return false;
}
return true;
}
Mappings:
#ToString
#Entity
#Table(name = "target")
public class Target {
#Id
#GeneratedValue(generator = "uuid")
#GenericGenerator(name = "uuid", strategy = "org.hibernate.id.UUIDGenerator")
#Column(name = "id", unique = true)
private UUID id;
#Column(name = "user_id")
private String userId;
#Column(name = "tenant_id")
private String tenantId;
#Column(name = "target_device_id")
private String targetDeviceId;
#Column(name = "target_type")
private TargetType targetType;
#OneToMany(mappedBy = "target", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY)
#JsonManagedReference
private List<EventHistory> events = new ArrayList<>();
public void addEvents(EventHistory event) {
events.add(event);
event.setTarget(this);
}
}
#Entity
#Data
#Table(name = "event_history")
public class EventHistory {
#Id
#GeneratedValue(generator = "uuid")
#GenericGenerator(name = "uuid", strategy = "org.hibernate.id.UUIDGenerator")
#Column(name = "id", unique = true)
private UUID id;
#Column(name = "active")
private boolean active;
#OneToMany(mappedBy = "event", cascade = CascadeType.ALL, orphanRemoval = true)
#JsonManagedReference
private List<EventTimestamp> timestamps = new ArrayList<>();
#ManyToOne(optional = false, cascade = CascadeType.ALL, fetch = FetchType.LAZY)
#JoinColumn(name = "target_id", nullable = false)
#OnDelete(action = OnDeleteAction.CASCADE)
#JsonBackReference
private Target target;
public void addTimestamps(EventTimestamp eventTimestamp) {
timestamps.add(eventTimestamp);
eventTimestamp.setEvent(this);
}
}
#Entity
#Data
#Table(name = "event_timestamp")
public class EventTimestamp {
#Id
#GeneratedValue(generator = "uuid")
#GenericGenerator(name = "uuid", strategy = "org.hibernate.id.UUIDGenerator")
#Column(name = "id", unique = true)
private UUID id;
#Column(name = "due_timestamp")
private Timestamp dueTimestamp;
#Column(name = "period")
private String period;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "event_id", nullable = false)
#JsonBackReference
private EventHistory event;
So my question is how to keep lazy loading inside transaction annotated functions?

My first assumption that the root cause had been the wrongly implemented repository function was wrong. The real issue was the #ToString annotation. This added the one-to-many event collection to toString(). While being inside the transaction and accessing the object, toString got invoked and the the collection was loaded.
Solution was to exclude the collection from toString via.
#OneToMany(mappedBy = "target", cascade = CascadeType.ALL, orphanRemoval = true)
#ToString.Exclude
private List<EventHistory> events;

I figured it out. The problem was ins the Repository code. The findBy method is expecting a List instead of a single object.
My original repository looked like this
#Repository
public interface TargetRepository extends CrudRepository<Target, UUID> {
Target findEventsByUserIdAndTenantIdAndTargetDeviceId(String userId, String tenantId, String targetId);
}
Changing it to the below version fixed it.
#Repository
public interface TargetRepository extends CrudRepository<Target, UUID> {
List<Target> findEventsByUserIdAndTenantIdAndTargetDeviceId(String userId, String tenantId, String targetId);
}

Related

post mapping rest api spring boot cannot get id

Im learning, and so far i created many to many bidirectional database - user can create many groups and group can have many users - and i cannot find a way for my GroupsController Post mapping to work, from my understanding, it requires to get firstly Users id, in order to set the right relationship in Join table for Group, because the relationship should be set only when user create/join group, not when user create sign up procedure. Postman throws 500 and intelliJ:
Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is java.lang.NullPointerException: Cannot invoke "java.lang.Long.longValue()" because the return value of "com.ilze.highlight.entity.Groups.getId()" is null] with root cause
java.lang.NullPointerException: Cannot invoke "java.lang.Long.longValue()" because the return value of "com.ilze.highlight.entity.Groups.getId()" is null
I use lombok - #Data, #Getter, therefore getId() should be available for use from Group class. My GroupsController with POST mapping when user decides to create a new group:
#RestController
#RequestMapping("api/groups") // pre-path
public class GroupsController{
#Autowired
private GroupsService groupsService;
#Autowired
private UserService userService;
#Autowired
private final GroupsRepository groupsRepository;
#Autowired
private UserRepository userRepository;
public GroupsController(GroupsRepository groupsRepository) {
this.groupsRepository = groupsRepository;
}
#GetMapping("/all-groups")
public List<Groups> getGroups(){
return (List<Groups>) groupsRepository.findAll();
}
#PostMapping("/user/{usersId}/create-group")
public ResponseEntity<Groups> createGroup(#PathVariable(value = "usersId") Long usersId, #RequestBody Groups groupRequest){
Groups group = userRepository.findById(usersId).map(users -> {
long groupsId = groupRequest.getId();
// add and create new group
users.addGroup(groupRequest);
return groupsRepository.save(groupRequest);
}).orElseThrow(() -> new ResourceNotFoundException("Not found user with id = " + usersId));
return new ResponseEntity<>(group, HttpStatus.CREATED);
}
}
Group database class:
#Data
#Entity
#AllArgsConstructor
#NoArgsConstructor
#Getter
#Table(name = "group_collection")
public class Groups {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "id")
private Long id;
#Column(name ="group_name", unique = true, nullable = false, length = 20)
private String groupName;
#Column(name = "size", nullable = false)
private int size;
#Column(name = "strict", nullable = false)
private boolean strict;
#Column(name = "open", nullable = false)
private boolean open;
#Column(name ="description", length = 300)
private String description;
#Column(name = "create_time", nullable = false)
private LocalDateTime createTime;
#ManyToMany(fetch = FetchType.LAZY,
cascade = {
CascadeType.PERSIST,
CascadeType.MERGE,
CascadeType.DETACH,
CascadeType.REFRESH
},
mappedBy = "groups")
#JsonIgnore
private Set<User> users = new HashSet<>();
public Set<User> getUsers() {
return users;
}
public void setUsers(Set<User> users) {
this.users = users;
}
}
And Users class for database:
#Data
#Entity
#AllArgsConstructor
#NoArgsConstructor
#Table(name = "users")
public class User {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "id")
private Long id;
#Column(name = "username", unique = true, nullable = false, length = 100)
private String username;
#Column(name = "password", nullable = false)
private String password;
#Column(name = "email", nullable = false)
private String email;
#Column(name = "create_time", nullable = false)
private LocalDateTime createTime;
#Enumerated(EnumType.STRING)
#Column(name = "role", nullable = false)
private Role role;
#Transient
private String accessToken;
#Transient
private String refreshToken;
#ManyToMany(fetch = FetchType.LAZY,
cascade = {
CascadeType.PERSIST,
CascadeType.MERGE,
CascadeType.DETACH,
CascadeType.REFRESH
})
#JoinTable(name = "groups_x_user",
joinColumns = { #JoinColumn(name = "users_id") },
inverseJoinColumns = {#JoinColumn(name = "groups_id")})
private Set<Groups> groups = new HashSet<>();
public void addGroup(Groups group) {
this.groups.add(group);
group.getUsers().add(this);
}
public void removeGroup(long id){
Groups group = this.groups.stream().filter(g ->
g.getId() == id).findFirst().orElse(null);
if(group != null){
this.groups.remove(group);
group.getUsers().remove(this);
}
}
For reference my GroupsService implementation:
#Service
public class GroupsServiceImpl implements GroupsService{
private final GroupsRepository groupsRepository;
public GroupsServiceImpl(GroupsRepository groupsRepository) {
this.groupsRepository = groupsRepository;
}
#Override
public Groups saveGroup(Groups group) {
group.setCreateTime(LocalDateTime.now());
return groupsRepository.save(group);
}
#Override
public Optional<Groups> findByGroupName(String groupName) {
return groupsRepository.findByGroupName(groupName);
}
}
You need to persist the object from request. And since you have Many-2-Many relation, you can insert related object from both sides. In your case: just add existing user to the newly created group
The method will look something like that:
#PostMapping("/user/{usersId}/groups")
public ResponseEntity<Groups> createGroup(#PathVariable(value = "usersId") Long usersId, #RequestBody Groups groupRequest) {
Groups createdGroup = userRepository.findById(usersId)
.map(user -> {
groupRequest.setId(null); // ID for new entry will be generated by entity framework, prevent override from outside
groupRequest.getUsers().add(user); // add relation
return groupsRepository.save(groupRequest);
}).orElseThrow(() -> new ResourceNotFoundException("Not found user with id = " + usersId));
return new ResponseEntity<>(createdGroup, HttpStatus.CREATED);
}

Null point exception on set spring JPA relationship

I have already seen topics with this question, but they did not help me. Maybe I didn't see something.
Below I attach the code and the error.
#Entity
#Table(name = "department")
#Data
#NoArgsConstructor
#AllArgsConstructor
#Builder
#EqualsAndHashCode
public class Department {
#Id
#GenericGenerator(name = "uuid2", strategy = "uuid2")
#GeneratedValue(strategy = GenerationType.IDENTITY, generator = "uuid2")
#Column(length = 36, nullable = false, updatable = false)
private UUID id;
#Column(name = "name", nullable = false)
private String name;
#Column(name = "description", nullable = false)
private String description;
#OneToMany(mappedBy = "department", cascade = CascadeType.ALL, fetch = FetchType.LAZY,
orphanRemoval = true)
private Set<User> userSet = new HashSet<>();
}
AND user
#Entity
#Table(name = "user")
#Data
#NoArgsConstructor
#AllArgsConstructor
#Builder
#EqualsAndHashCode
public class User {
#Id
#GenericGenerator(name = "uuid2", strategy = "uuid2")
#GeneratedValue(strategy = GenerationType.IDENTITY, generator = "uuid2")
#Column(length = 36, nullable = false, updatable = false)
private UUID id;
private String firstName;
private String lastName;
private Integer age;
#ManyToOne
#JoinColumn(name = "dep_id", nullable = true)
private Department department;
#ManyToMany
#JoinTable(
name = "device_devices",
joinColumns = #JoinColumn(name = "user_id"),
inverseJoinColumns = #JoinColumn(name = "device_id"))
Set<Device> devices = new HashSet<>();
}
and service
#Override
public List<DepartmentDto> getAllDepartment() {
List<Department> all = departmentRepository.findAll();
return all.stream().map(mapper::toDepartmentDto).collect(Collectors.toList());
}
#Override
public UUID createDepartment(DepartmentDto departmentDto) {
Department entity = mapper.DtoToDepartment(departmentDto);
return departmentRepository.save(entity).getId();
}
#Override
public void deleteDepartment(UUID id) {
departmentRepository.deleteById(id);
}
#Override
#Transactional
public void addUserToDepartment(UUID departmentId,UUID userId){
Department department = departmentRepository.findById(departmentId).orElseThrow(DepartmentNotFoundException::new);
User user = userRepository.findById(userId).orElseThrow(UserNotFoundException::new);
department.getUserSet().add(user); // ERROR java.lang.NullPointerException: null
user.setDepartment(department);
}
I think I did something wrong. I tried writing Cascade.ALL but it didn't help me. I don't think I fully understand the concept of a link collection yet. I don't like that in my code, I add the user to the department, and then I add the department to the user. Probably it can be done in one action.
Task.
I want to make a department and 2 functions. Add a user to the department and remove users from the department. in such a way that the contempt of the users from the department the user himself was not removed.
I will be glad to hear your comments
if you use #Builder annotation on top of the class
and you want to set default value for a field,
you must put annotation #Builder.Default on top of field
#Builder.Default
Set<Device> devices = new HashSet<>();
otherwise devices will always be NULL when the builder builds the object

Spring boot JPA related entity clarification

I have an entity like Process, which will be created by , updated by one user. When I try to apply the filter. I have created the foreign key relationship in the database. Now, when I use the JPA Specification to apply dynamic filter, I am getting exception as
No property CREATED found for type Process!
#Table(name = "process")
#Entity
public class Process {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "PROCESS_ID")
#JsonProperty("id")
private Long id = null;
#NotNull
#Column(name = "NAME")
#JsonProperty("name")
private String name = null;
#Column(name = "CREATED_BY", updatable = false)
#JsonProperty("createdBy")
private Long createdBy = null;
#Column(name = "updatedBy", nullable = true)
#JsonProperty("updatedBy")
private Long updatedBy = null;
}
Hence, I Added the entity relationship mapping in the process entity as given below,
Now, I am getting below error. I am new to JPA and hibernate, the relation mapping is very confusing, kindly help.
#Table(name = "process")
#Entity
public class Process {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "PROCESS_ID")
#JsonProperty("id")
private Long id = null;
#NotNull
#Column(name = "NAME")
#JsonProperty("name")
private String name = null;
#Column(name = "CREATED_BY", updatable = false)
#JsonProperty("createdBy")
private Long createdBy = null;
#Column(name = "updatedBy", nullable = true)
#JsonProperty("updatedBy")
private Long updatedBy = null;
//newly added below properties so that there will be no error when fetching data
#OneToOne(targetEntity = UserDetails.class, fetch = FetchType.LAZY, mappedBy = "id")
private UserDetails CREATED;
#OneToOne(targetEntity = UserDetails.class, fetch = FetchType.LAZY, mappedBy = "id")
private UserDetails UPDATED;
}
Now, I am getting the below error
Referenced property not a (One|Many)ToOne: com.app.users.details.domain.UserDetails.id in mappedBy of com.app.scenarios.domain.Process.CREATED
Kindly let me know what i am doing wrong. I have a process which can be created by a user and can be updated by a user. In DB, I am having a foreign key relationship for process and userdetails entity.
EDIT
Code to get the filtered data from DB using JPA Specification
Page<process> result = this.processDao.findAll(getprocessGridData(processSearchCondition.getprocessName()), pageRequest);
private static Specification<process> getprocessGridData(String processName) {
return (Specification<process>) (root, query, criteriaBuilder) -> (
criteriaBuilder.like(root.get("name"), processName)
);
}
I guess what you actually want is this:
#Table(name = "process")
#Entity
public class Process {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "PROCESS_ID")
#JsonProperty("id")
private Long id;
#NotNull
#Column(name = "NAME")
#JsonProperty("name")
private String name;
#OneToOne(fetch = FetchType.LAZY)
#jOINColumn(name = "CREATED_BY", updatable = false)
private UserDetails createdBy;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "UPDATED_BY", nullable = true)
private UserDetails updatedBy;
}

Not able to delete in #OneToMany relationship spring data jpa

In my spring boot project, I have one LineItem entity below is the code
#Entity
#Table(name = "scenario_lineitem")
#Data
#NoArgsConstructor
public class LineItem implements Cloneable {
private static Logger logger = LoggerFactory.getLogger(GoogleConfigConstant.class);
#Id
#GeneratedValue(strategy = IDENTITY)
private BigInteger lineItemId;
#Column
private String name;
#OneToMany(fetch = FetchType.LAZY, cascade = { CascadeType.ALL, CascadeType.PERSIST, CascadeType.MERGE })
#JoinColumn(name = "line_item_meta_id")
private List<QuickPopValue> quickPopValues;
}
Another entity is
#Entity
#Table(name = "quick_pop_value")
#Data
#NoArgsConstructor
public class QuickPopValue implements Cloneable {
#Id
#GeneratedValue(strategy = IDENTITY)
#Column(name = "quick_pop_value_id", columnDefinition = "bigint(20)", unique = true, nullable = false)
private BigInteger quickPopValueId;
#Column(name = "column_name")
private String columnName;
#Column(name = "value")
private String value;
#Column(name = "formula", columnDefinition = "longtext")
private String formula;
}
Now I am trying to delete QuickPopValue one by one but it's not getting deleted and not getting any exception as well.
Below is the delete code :
List<QuickPopValue> quickPopValues = sheetRepository.findByColumnName(columnName);
for (QuickPopValue qpValue : quickPopValues) {
quickPopValueRepository.delete(qpValue);
}
Such behavior occurs when deleted object persisted in the current session.
for (QuickPopValue qpValue : quickPopValues) {
// Here you delete qpValue but this object persisted in `quickPopValues` array which is
quickPopValueRepository.delete(qpValue);
}
To solve this you can try delete by id
#Modifying
#Query("delete from QuickPopValue t where t.quickPopValueId = ?1")
void deleteQuickPopValue(Long entityId);
for (QuickPopValue qpValue : quickPopValues) {
quickPopValueRepository.deleteQuickPopValue(qpValue.getQuickPopValueId());
}

Hibernate unexpected deletes when finding entities

We have a Java ee application running on JBoss 6.4 GA using JPA and Hibernate with the following entities:
#Entity
#SequenceGenerator(name = "sequence", sequenceName="SEQ_CAMPAIGNS_ID",allocationSize = 1)
#Table(name = "CAMPAIGN")
public class CampaignEntity implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "sequence")
#Column(name = "ID")
private Long id;
#Column(name = "NAME")
private String name;
#Column(name = "IS_ACTIVE", nullable = false)
private boolean active;
#Column(name = "START_DATE", nullable = false)
private Date startDate;
#Column(name = "END_DATE", nullable = false)
private Date endDate;
#Column(name = "LEGAL_ENTITY_ID", nullable = false)
private Integer legalEntityId;
#Column(name = "DEPARTMENT", nullable = false)
#Enumerated(value = EnumType.STRING)
private Department department;
#Column(name = "CATEGORY", nullable = false)
#Enumerated(value = EnumType.STRING)
private Category category;
#Embedded
CampaignConditionsEntity campaignConditions;
#OneToMany(cascade = CascadeType.ALL, mappedBy = "campaign", orphanRemoval = true)
#OrderBy
private List<CodeEntity> campaignCodes;
public CampaignEntity() {
}
And the following CampaignConditionsEntity:
#Embeddable
public class CampaignConditionsEntity implements Serializable {
private static final String CAMPAIGN_ID = "CAMPAIGN_ID";
#ElementCollection(fetch = FetchType.EAGER)
#CollectionTable(name = "CAMPAIGN_COND_TRIP_TYPE", joinColumns = #JoinColumn(name = CAMPAIGN_ID))
private Set<TripTypeConditionEntity> tripTypeConditions;
And the following CodeEntity:
#Entity
#Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
#SequenceGenerator(name = "sequence", sequenceName = "SEQ_CODES_ID", allocationSize = 1)
public abstract class CodeEntity implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "sequence")
#Column(name = "ID", nullable = false)
private Long id;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "CAMPAIGN_ID")
private CampaignEntity campaign;
#OneToOne(mappedBy = "code", cascade = CascadeType.ALL, fetch = FetchType.LAZY, optional = false, orphanRemoval = true)
private DiscountEntity discount;
#Column(name = "MAX_USAGES", nullable = false)
private Integer maxUsages;
#Column(name = "UNLIMITED_USAGES", nullable = false)
private boolean unlimitedUsages;
#Column(name = "NEGATIVE_SH", nullable = false)
private boolean negativeSH;
#Column(name = "UNIQUE_BUYER", nullable = false)
private boolean uniqueBuyer;
#Column(name = "START_DATE")
private Date startDate;
#Column(name = "END_DATE")
private Date endDate;
#Embedded
private CodeConditionsEntity codeConditions;
public CodeEntity() {
}
This is the CodeConditionsEntity:
#Embeddable
public class CodeConditionsEntity implements Serializable {
private static final String CODE_ID = "CODE_ID";
#ElementCollection(fetch = FetchType.EAGER)
#CollectionTable(name = "CODE_COND_TRIP_TYPE", joinColumns = #JoinColumn(name = CODE_ID))
private Set<TripTypeConditionEntity> tripTypeConditions;
#ElementCollection(fetch = FetchType.EAGER)
#CollectionTable(name = "CODE_COND_CARRIERS", joinColumns = #JoinColumn(name = CODE_ID))
private Set<CarrierConditionEntity> carrierConditions;
This is the CarrierConditionEntity:
#Embeddable
public class CarrierConditionEntity implements Serializable {
#Column(name = "CARRIER", nullable = false, length = 3)
private String carrierCode;
#Column(name = "IS_INCLUDED", nullable = false)
private boolean included;
The problem is that in the logs we are finding unexpected deletes when the only operation that we are doing are finds of particular campaign entities.
In the production logs we find the following deletes
Hibernate: delete from CODE_COND_CARRIERS where CODE_ID=? and CARRIER=? and IS_INCLUDED=?
do you have any suggestion?
thanks
I have some suggestions :)
Be aware of what is a Persistence Context (EntityManager instance in JPA terminology / Session in Hibernate one), the entity lifecycle and transaction scope (unit of work)
Do not mutate entity state if you don't expect the changes to be reflected in database, or at least detach the entity before mutating it.
Mark your transaction as "readOnly" if you only fetch data in the related unit of work. (beware that if you have many "Transactional" methods joining the same physical transaction, the flag is set by the surrounding one and cannot be overridden by inner logical transactions). That way the EntityManager won't be flushed at the end of the transaction and pending changes won't be persisted to the database.
You can track the method triggering the unexpected deletion using an EntityListener on the related entity and printing the current strackTrace (new Throwable().printStackTrace()/ log(new Throwable()) in the PreRemove method
I found where was the problem:
The problem was that the Entities didn't have the equals() and the hashcode() implemented. Also there were entities that have a #PostLoad that modified the entity after loading it from database. Then in this situation Hibernate though that there was a change in those entities that didn't have the equals and the hashcode, and then it delete all of them and inserted again in the database (to have the same entities before the query)
Adding the equals and hashcode methods and deleting postload removed the unexpected deletes and inserts from the logs.
regards

Categories