I have an enterprise project configured by spring mvc4 + hibernate5 that all of its relation are eager and its performance is very bad...So I am transforming all eager relations to lazy step by step...But I see many errors in each step...and it works sometimes properly and sometimes not....
in this example HeaderFromStore is an instnace of RequestHeaders and a child of RequestLine. DeliveryPoint is child of requestHeader and I don't want to fetch deliveryPoint of requestHeader...But if don't use it in select query it couldn't fetch HeaderFromStore !!
I used this query and I get error!
select m from MAMRequestLines m join fetch m.mamRequestHeaders r
left join fetch m.requestHeaderFromStore rr where m.id =:id
If I use this query I don't get error
select m from MAMRequestLines m join fetch m.mamRequestHeaders r
left join fetch m.requestHeaderFromStore rr
join fetch rr.mamDeliveryPoints
left join fetch r.mamDeliveryPoints
join fetch where m.id =:id
RequestLine.java
#Entity(name = "RequestLines")
#Table(name = "_REQUEST_LINES")
//#JsonIdentityInfo(generator=ObjectIdGenerators.PropertyGenerator.class, property = "#id")
public class RequestLines extends Entity implements Serializable {
#JsonInclude(JsonInclude.Include.NON_EMPTY)
private RequestHeaders requestHeaders;
#JsonInclude(JsonInclude.Include.NON_EMPTY)
private RequestHeaders requestHeaderFromStore;
#Id
#GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "_REQUEST_Line_SEQ")
#SequenceGenerator(name = "_REQUEST_Line_SEQ", sequenceName = "_REQUEST_Line_SEQ")
#Column(name = "REQUEST_LINE_ID")
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "REQUEST_HEADER_ID", nullable = false)
public RequestHeaders getRequestHeaders() {
return RequestHeaders;
}
public void setRequestHeaders(RequestHeaders requestHeaders) {
this.RequestHeaders = requestHeaders;
}
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "REQUEST_HEADER_FROM_STORE")
public RequestHeaders getRequestHeaderFromStore() {
return requestHeaderFromStore;
}
public void setRequestHeaderFromStore(RequestHeaders requestHeaderFromStore) {
this.requestHeaderFromStore = requestHeaderFromStore;
}
}
RequestHeader.java
#Entity(name = "RequestHeaders")
#Table(name = "REQUEST_HEADERS")
//#JsonInclude(JsonInclude.Include.NON_EMPTY)
public class RequestHeaders extends Entity implements Serializable {
private long id;
// #JsonInclude(JsonInclude.Include.NON_EMPTY)
// #JsonIgnore
private DeliveryPoints DeliveryPoints;
#JsonIgnore
private Set<RequestLines> RequestLinesSet;
#Id
#GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "_REQUEST_HEADERS_SEQ")
#SequenceGenerator(name = "_REQUEST_HEADERS_SEQ", sequenceName = "_REQUEST_HEADERS_SEQ")
#Column(name = "REQUEST_HEADER_ID")
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "DELIVERY_POINT_ID", nullable = false)
public DeliveryPoints getDeliveryPoints() {
return DeliveryPoints;
}
public void setDeliveryPoints(DeliveryPoints DeliveryPoints) {
this.DeliveryPoints = DeliveryPoints;
}
#OneToMany(mappedBy = "RequestHeaders", fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true)
#OnDelete(action = OnDeleteAction.CASCADE)
public Set<RequestLines> getRequestLinesSet() {
return RequestLinesSet;
}
public void setRequestLinesSet(Set<RequestLines> RequestLinesSet) {
this.RequestLinesSet = RequestLinesSet;
}
}
exception:
No serializer found for class
org.hibernate.proxy.pojo.javassist.JavassistLazyInitializer and no
properties discovered to create BeanSerializer (to avoid exception,
disable SerializationFeature.FAIL_ON_EMPTY_BEANS) ) (through reference
chain:
domain.RequestLine["HeaderFromStore"]->.domain.RequestHeaders["DeliveryPoint"]->domain.DeliveryPoint_$$_jvst393_f["handler"])
notice that I used JsonIgnore and JsonInclude(on fields and class) but none of them doesn't work...
Edit:
I finally found this solution to avoid exception and ignoring unwanted properties.
I added this part of code to WebMvcConfig extends WebMvcConfigurerAdapter class:
{
public MappingJackson2HttpMessageConverter jacksonMessageConverter(){
MappingJackson2HttpMessageConverter messageConverter = new MappingJackson2HttpMessageConverter();
ObjectMapper mapper = new ObjectMapper();
//Registering Hibernate4Module to support lazy objects
mapper.registerModule(new Hibernate4Module());
messageConverter.setObjectMapper(mapper);
return messageConverter;
}
#Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
//Here we add our custom-configured HttpMessageConverter
converters.add(jacksonMessageConverter());
super.configureMessageConverters(converters);
}
But I have another problem now...all post requests will receive with null properties in request body ....for example in this code all properties of "requestHeaders" in input is null or empty!
#RequestMapping(value = "/requestHeader/", method = RequestMethod.POST, consumes = {"application/json"})
public ResponseEntity<Void> createRequestHeaders(#RequestBody RequestHeaders requestHeaders, UriComponentsBuilder ucBuilder) {
requestHeaders.setDeliveryPoints(deliveryPointsService.find(requestHeaders.getDeliveryPointsId()));
requestHeadersService.add(requestHeaders);
HttpHeaders headers = new HttpHeaders();
return new ResponseEntity<Void>(headers, HttpStatus.CREATED);
}
you should add each entities which have relation this annotation at the top of class definition.
#JsonIgnoreProperties({"hibernateLazyInitializer", "handler"})
details are explained here and here
I hope these solves your problem.
Related
My problem is that Hibernate does not persist nested entities given in entity.
Consider following entities:
PollEntity
#Table(name = "\"poll\"")
#Data
#AllArgsConstructor
#NoArgsConstructor
#Entity
#EqualsAndHashCode(of = "id")
#Builder
public class PollEntity {
#Transient
public OptionEntity addOption(OptionEntity pollOptionEntity) {
if(options == null)
options = new HashSet<>();
options.add(pollOptionEntity);
pollOptionEntity.setPoll(this);
return pollOptionEntity;
}
#Transient
public OptionEntity dropOption(OptionEntity pollOptionEntity) {
options.remove(pollOptionEntity);
pollOptionEntity.setPoll(null);
return pollOptionEntity;
}
#Id
#Column(name = "id")
private UUID id;
#Column(name = "author")
#UUIDv4
private UUID author;
#Column(name = "poll_question")
#Size(max = 1000)
private String pollQuestion;
#OneToMany(fetch = FetchType.EAGER, mappedBy = "poll", cascade = CascadeType.MERGE)
#Builder.Default
#Valid
private Set<OptionEntity> options;
}
OptionEntity
#Table(name = "\"option\"")
#Data
#AllArgsConstructor
#NoArgsConstructor
#Entity
#EqualsAndHashCode(of = "id")
#Builder
public class OptionEntity {
#Id
#GeneratedValue(generator = "UUID")
#Column(name = "id")
#GenericGenerator(name = "UUID", strategy = "org.hibernate.id.UUIDGenerator")
private UUID id;
#JoinColumn(name = "poll_id")
#ManyToOne
private PollEntity poll;
#Column(name = "option")
#Size(max = 1000)
#NotNull
private String option;
}
And here's service method:
#Transactional(rollbackFor = Throwable.class)
public void createPoll(#Valid PollEntity pollEntity) throws ValidationException {
validationService.validateOrThrow(pollEntity);
if (pollRepository.findById(pollEntity.getId()).isPresent())
throw new ValidationException("Invalid id", Map.of("id", "Poll with id (" + pollEntity.getId() + ") already exists"));
pollEntity = validationService.validateAndSave(pollRepository, pollEntity);
And corresponding test:
#Test
public void createPollTest() throws ValidationException {
var uuid = UUID.randomUUID();
var pollOption1 = OptionEntity.builder()
.option("Test option 1")
.build();
var pollOption2 = OptionEntity.builder()
.option("Test option 2")
.build();
var pollOption3 = OptionEntity.builder()
.option("Test option 3")
.build();
var poll = PollEntity.builder()
.id(uuid)
.pollQuestion("Test question")
.author(UUID.randomUUID())
.build();
poll.addOption(pollOption1);
poll.addOption(pollOption2);
poll.addOption(pollOption3);
pollService.createPoll(poll);
}
Which gives following output in database
poll
2e565f50-7cd4-4fc9-98cd-49e0f0964487 feae5781-ff07-4a21-9292-c11c4f1a047d Test question
option
c786fe5d-632d-4e94-95ef-26ab2af633e7 fc712242-8e87-41d8-93f2-ff0931020a4a Test option 1
and rest options ended up unpersisted.
I've also used to create options in separate method
#Transactional(propagation= Propagation.REQUIRES_NEW, rollbackFor = Throwable.class)
public Set<OptionEntity> createOptions(#Valid Set<OptionEntity> pollOptionsEntities) throws ValidationException {
for (var pollOption : pollOptionsEntities) {
validationService.validateAndSave(pollOptionsRepository, pollOption);
}
return pollOptionsEntities;
}
and option entities were getting produced but had to switch to persisting from built-in entity methods due to errors with persisting poll entity.
Database schema looks like this:
CREATE TABLE "poll"
(
"id" UUID PRIMARY KEY,
"author" UUID NOT NULL,
"poll_question" VARCHAR(1000) NOT NULL
)
CREATE TABLE "option"
(
"id" UUID PRIMARY KEY,
"poll_id" UUID NOT NULL REFERENCES "poll" ("id") ON DELETE CASCADE
"option" VARCHAR(1000) NOT NULL
)
What are the possible approaches to try?
UPD 1
Having considered various looking-alike questions (1,2,3,4,5)
I've came up with this addition to entity which suppose make entities persistence regardless of actual value and still having only one option in output. What was done wrong?
#Override
public boolean equals(Object object) {
if ( object == this ) {
return false;
}
if ( object == null || object.getClass() != getClass() ) {
return false;
}
final OptionEntity other = OptionEntity.class.cast( object );
if ( getId() == null && other.getId() == null ) {
return false;
}
else {
return false;
}
}
#Override
public int hashCode() {
final HashCodeBuilder hcb = new HashCodeBuilder( 17, 37 );
if ( id == null ) {
while (getOptions().iterator().hasNext())
hcb.append( getOptions().iterator().next() );
}
else {
hcb.append( id );
}
hcb.append( options );
return hcb.toHashCode();
}
So the answer were quite trivial all the way long.
In order to overcome this the only thing that should be done is to change container: Set<OptionEntity> to List<OptionEntity>.
Hope this will not produce some hard-to-tackle bugs but if it can - please add comment.
Because in my case uniqueness was not strict requirement, ended up with this:
PollEntity:
#Table(name = "\"poll\"")
#Data
#AllArgsConstructor
#NoArgsConstructor
#Entity
#Builder
public class PollEntity {
#Transient
public OptionEntity addOption(OptionEntity pollOptionEntity) {
options.add(pollOptionEntity);
pollOptionEntity.setPoll(this);
return pollOptionEntity;
}
#Transient
public OptionEntity dropOption(OptionEntity pollOptionEntity) {
options.remove(pollOptionEntity);
pollOptionEntity.setPoll(null);
return pollOptionEntity;
}
#Id
#Column(name = "id")
private UUID id;
#Column(name = "status")
#Enumerated(EnumType.STRING)
#NotNull
private Status status;
#Column(name = "author")
#UUIDv4
private UUID author;
#Column(name = "poll_question")
#Size(max = 1000)
private String pollQuestion;
#OneToMany(fetch = FetchType.EAGER, mappedBy = "poll", cascade = CascadeType.MERGE)
#Builder.Default
#Valid
private List<OptionEntity> options = new ArrayList<>();
}
I'am trying to provide Application on Spring with Lazy-Fetch relation between Entities.
Model "User":
Entity
#Table(name = "users")
#Component
public class User {
#Id
#SequenceGenerator(name = "userseq", sequenceName = "userseq", allocationSize = 1)
#GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "userseq")
private Integer id;
// Some fields/getters/setters
#OneToMany(fetch = FetchType.LAZY, mappedBy = "user")
private List<Tracker> trakers;
}
Model "Trackers"
#Entity
#Table(name = "trackers")
#Component
public class Tracker {
#Id
#SequenceGenerator(name = "tracker_seq", sequenceName = "tracker_seq", allocationSize = 1)
#GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "tracker_seq")
private Integer id;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "user_id", insertable=false, updatable=false)
#OnDelete(action = OnDeleteAction.CASCADE)
private User user;
// Some fields/getters/setters
}
Inherited JPA-repository with #EntityGraph. By this I'am trying to provide select user with all trackers, related with:
#Repository
#Transactional(readOnly = true)
public interface CrudUserRepository extends JpaRepository<User, Integer> {
#EntityGraph (attributePaths = {"trackers"}, type = EntityGraph.EntityGraphType.LOAD)
#Query("SELECT u FROM User u WHERE u.id=?1")
User getByIdWithTrackers(int id);
}
Repository-class:
#Repository
public class AnketUserRepository implements UserRepository {
#Autowired
private CrudUserRepository crudRepository;
#Override
public User getByIdWithoutTrackers(int id) {
return crudRepository.findById(id).orElse(null);
}
#Override
public User getByIdWithTrackers(int id){
return crudRepository.getByIdWithTrackers(id);
}
}
And controller:
#RestController ("userRestController")
#RequestMapping(value = UserRestController.USER_URL, produces =
MediaType.APPLICATION_JSON_VALUE)
public class UserRestController extends AbstractUserController {
public static final String USER_URL = "/customers";
#GetMapping("/{id}")
public User getByIdWithoutTrackers(#PathVariable int id) {
return super.getByIdWithoutTrackers(id);
}
#GetMapping("/{id}/withTrackers")
public User getByIdWithTrackers(#PathVariable int id) {
return super.getByIdWithTrackers(id);
}
}
Query "/customers/1" works fine. It returns all customers without trackers (Lazy-Fetch, accordingly).
But "/customers/1/withTrackers" returns the following exception:
lang.IllegalArgumentException: Unable to locate Attribute with the the given name [trackers] on this ManagedType [ru.spb.model.User]"}
Oh, its a stupid mistake. In User I write "trakers". But in CrudUserRepository at attributePaths "traCkers".
I have spring boot application which use spring data and hibernate to fetch and insert data to database.
I have one-to-many table relation:
#Entity
#Data
#EqualsAndHashCode(of = { "id" })
#Table(name = "direction")
public class Direction {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "id", nullable = false)
private Long id;
#Column(name = "name")
private String name;
}
and
#Entity
#Table(name = "subdivision")
#Data
#EqualsAndHashCode()
public class Subdivision {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "id", nullable = false)
private Long id;
#Column(name = "name")
private String name;
#ManyToOne()
#JoinColumn(name = "direction_id", referencedColumnName = "id")
private Direction direction;
}
I have lombok plugin for boilarplate code generation.
I also have repository
public interface SubdivisionRepository extends CrudRepository<Subdivision, Long> {
List<Subdivision> findAll();
List<Subdivision> findByDirection(Direction direction);
}
and service
#Service
public class SubdivisionServiceImpl implements SubdivisionService {
#Autowired
private SubdivisionRepository subdivisionRepository;
#Override
public List<Subdivision> findAll() {
return subdivisionRepository.findAll();
}
#Override
public Subdivision findById(Long id) {
return subdivisionRepository.findById(id).get();
}
#Override
#Transactional
public void save(Subdivision subdivision) {
subdivisionRepository.save(subdivision);
}
#Override
public List<Subdivision> findByDirection(Direction direction) {
return subdivisionRepository.findByDirection(direction);
}
}
That's all. Then I try to update subdirection by changing direction type it shows hibernate exception: Error during managed flush [org.hibernate.HibernateException: identifier of an instance of com.entity.Direction was altered from 2 to 3]
I found the same question on stackoverflow but nothing suggested helped.
I tried to change fetch type and cascade type but it didn't helped.
Does anyone have solution?
P.S Here the code how I update entity
public void updateSubdivision(Subdivision subdivision){
Direction d = directionService.findById(subdivision.getDirection().getId());
Subdivision s = new Subdivision();
s.setDirection(d);
s.setName(subdivision.getName());
s.setId(subdivision.getId());
subdivisionService.save(s);
}
It's controller method
I have a Spring Data Rest repository
public interface ProjectRepository extends CrudRepository<Project, Integer> {}
for the following entity:
#javax.persistence.Entity
#Table(name = "project", uniqueConstraints = {#UniqueConstraint(columnNames = {"owner_id", "title"})})
public class Project {
#Id
#Column(name = "project_id")
#GeneratedValue(strategy = GenerationType.AUTO)
private int id;
...
#ManyToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
#JoinTable(name = "project_document", joinColumns = {
#JoinColumn(name = "project_id", nullable = false, updatable = false) },
inverseJoinColumns = { #JoinColumn(name = "document_id",
nullable = false, updatable = false) })
private Set<Document> documents;
...
}
I want to override the POST handler of the nested documents collection and am following the recommended approach.
#RepositoryRestController
public class DocumentController {
#RequestMapping(value = "/projects/{projectId}/documents", method = RequestMethod.POST)
public Document postDocument(
final #PathVariable int projectId,
final #RequestPart("file") MultipartFile documentFile,
final #RequestPart("description") String description
) throws IOException {
...
}
}
But when I fire up the nested POST, it still uses the original Spring generated POST handler and throws unsupported media-type error.
When I change #RepositoryRestController to #RestController, the correct POST handler is used, but the Spring generated CRUD methods for documents subresource of project are not exported.
Try something like this:
#RequiredArgsConstructor
#RepositoryRestController
#RequestMapping("/projects/{id}")
public class ProjectsController {
private final #NonNull DocumentRepository documentRepository;
#PostMapping("/documents")
public ResponseEntity<?> postDocument(#PathVariable("id") Project project, #RequestBody Document document) {
if (project == null) {
throw new Exception("Project is not found!");
}
if (document == null) {
throw new Exception("Document is not found");
}
Document savedDocument = documentRepository.save(document.setProject(project));
return new ResponseEntity<>(new Resource<>(savedDocument), CREATED);
}
}
Working example.
I have a following error:
com.mysql.jdbc.exceptions.jdbc4.MySQLIntegrityConstraintViolationException: Cannot delete or update a parent row: a foreign key constraint fails (`spindledb`.`section`, CONSTRAINT `FK_ftoru9cp83n512p9is8x3vo53` FOREIGN KEY (`scenario_id`) REFERENCES `scenario` (`scenario_id`))
Here are my classes:
Scenario:
#Entity
#Table(name = "scenario")
public class Scenario {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "scenario_id")
private int id;
#Column(name = "title", nullable = false)
private String title;
#NotNull
#DateTimeFormat(pattern = "dd/MM/yyyy")
#Column(name = "creation_date", nullable = false)
#Type(type = "org.jadira.usertype.dateandtime.joda.PersistentLocalDate")
private LocalDate creationDate;
#ManyToOne
#LazyCollection(LazyCollectionOption.FALSE)
#JoinColumn(name = "id", nullable = false)
private User user;
#OneToMany(mappedBy = "scenario", orphanRemoval = true)
#LazyCollection(LazyCollectionOption.FALSE)
private Set<Plot> plotList = new HashSet<Plot>();
#OneToMany(mappedBy = "scenario", orphanRemoval = true)
#LazyCollection(LazyCollectionOption.FALSE)
private Set<Character> characterList = new HashSet<Character>();
#OneToMany(mappedBy = "scenario", cascade=CascadeType.ALL, orphanRemoval = true)
#LazyCollection(LazyCollectionOption.FALSE)
#OrderBy("sequence ASC")
private Set<Section> sectionList = new HashSet<Section>();
Section:
#Entity
#Table(name = "section")
public class Section {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "section_id")
private int id;
#Size(min = 4, max = 50)
#NotNull
#Column(name = "name")
private String name;
#NotNull
#Column(name = "type")
private String type = SectionType.TEXT.getSectionType();
#Column(name = "visibility")
private boolean visibility;
#NotNull
#Column(name = "sequence")
private int sequence;
#ManyToOne (cascade=CascadeType.ALL)
#LazyCollection(LazyCollectionOption.FALSE)
#JoinColumn(name = "scenario_id", nullable = false)
private Scenario scenario;
Controller:
#RequestMapping(value = { "/delete-{id}-scenario" }, method = RequestMethod.GET)
public String deleteScenario(#PathVariable int id) {
scenarioService.deleteScenarioById(id);
return "redirect:/home";
}
Scenario service:
#Service("scenarioService")
#Transactional
public class ScenarioServiceImpl implements ScenarioService {
#Autowired
private ScenarioDao dao;
#Override
public Scenario findById(int id) {
return dao.findById(id);
}
#Override
public void saveScenario(Scenario scenario) {
dao.saveScenario(scenario);
}
public void updateScenario(Scenario scenario) {
Scenario entity = dao.findById(scenario.getId());
if(entity!=null){
entity.setTitle(scenario.getTitle());
entity.setCreationDate(scenario.getCreationDate());
}
}
#Override
public void deleteScenarioById(int id) {
dao.deleteScenarioById(id);
}
Dao
#Repository("scenarioDao")
public class ScenarioDaoImpl extends AbstractDao<Integer, Scenario> implements ScenarioDao {
#Override
public Scenario findById(int id) {
return getByKey(id);
}
#Override
public void saveScenario(Scenario scenario) {
persist(scenario);
}
#Override
public void deleteScenarioById(int id) {
Query query = getSession().createSQLQuery("delete from scenario where id = :id");
query.setString("id", ""+id);
query.executeUpdate();
}
I understand that the problem is that there may be a Section that can not exist without scenario. Right now however section table in database is empty and I still can't remove Scenario. Thanks for advice
Deleting an entity via Query would bypass any Cascade settings you put via annotation.
I would suggest find the entity first by id, then delete the entity object:
Object scenario = session.load(Scenario.class, id);
if (scenario != null) {
session.delete(scenario);
}
use cascade=CascadeType.ALL with all #ManyToOne relations in class Scenario because if you are going to delete any Scenario from database it must not be referenced any where in data base.
the other way to delete is.
Serializable id = new Long(1); //your id
Object persistentInstance = session.load(Scenario.class, id);
if (persistentInstance != null) {
session.delete(persistentInstance);
}