Spring JPA Auditing empty createdBy
Asked Answered
W

3

5

I am using the auditing capabilities of Spring Data and have a class similar to this:

@Entity
@Audited
@EntityListeners(AuditingEntityListener.class)
@Table(name="Student")
public class Student {
    @Id
    @GeneratedValue (strategy = GenerationType.AUTO)
    private Long id;

    @CreatedBy
    private String createdBy;

    @CreatedDate
    private Date createdDate;

    @LastModifiedBy
    private String lastModifiedBy;

    @LastModifiedDate
    private Date lastModifiedDate;
...

Now, I believe I have configured auditing fine because I can see that createdBy, createdDate, lastModifiedBy and lastModifiedDate all are getting the correct values when I update the domain objects.

However, my problem is that when I update an object I am losing the values of createdBy and createdDate. So, when I first create the object I have all four values, but when I update it createdBy and createdDate are nullified ! I am also using the Hibernate envers to keep a history of the domain objects.

Do you know why do I get this behavior ? Why do createdBy and createdDate are empty when I update the domain object ?

Update: To answer @m-deinum 's questions: Yes spring data JPA is configured correctly - everything else works fine - I really wouldn't like to post the configuration because as you udnerstand it will need a lot of space.

My AuditorAwareImpl is this

@Component
public class AuditorAwareImpl implements AuditorAware {
    Logger logger = Logger.getLogger(AuditorAwareImpl.class);

    @Autowired
    ProfileService profileService;

    @Override
    public String getCurrentAuditor() {
        return profileService.getMyUsername();
    }
}

Finally, here's my update controller implementation:

    @Autowired  
    private StudentFormValidator validator;
    @Autowired
    private StudentRepository studentRep;

@RequestMapping(value="/edit/{id}", method=RequestMethod.POST)  
public String updateFromForm(
         @PathVariable("id")Long id,
         @Valid Student student, BindingResult result,
         final RedirectAttributes redirectAttributes)   {  

     Student s =  studentRep.secureFind(id); 
     if(student == null || s == null) {
         throw new ResourceNotFoundException();
     }
     validator.validate(student, result);
     if (result.hasErrors()) {  
         return "students/form";
     } 
     student.setId(id);
     student.setSchool(profileService.getMySchool());
     redirectAttributes.addFlashAttribute("message", "Επιτυχής προσθήκη!");
     studentRep.save(student);
     return "redirect:/students/list";  
}  

Update 2: Please take a look at a newer version

@RequestMapping(value="/edit/{id}", method=RequestMethod.GET)  
     public ModelAndView editForm(@PathVariable("id")Long id)  {  
         ModelAndView mav = new ModelAndView("students/form");  
         Student student =  studentRep.secureFind(id); 
         if(student == null) {
             throw new ResourceNotFoundException();
         }
         mav.getModelMap().addAttribute(student);
         mav.getModelMap().addAttribute("genders", GenderEnum.values());
         mav.getModelMap().addAttribute("studentTypes", StudEnum.values());
         return mav;  
     }  

     @RequestMapping(value="/edit/{id}", method=RequestMethod.POST)  
     public String updateFromForm(
             @PathVariable("id")Long id,
             @Valid @ModelAttribute Student student, BindingResult result,
             final RedirectAttributes redirectAttributes, SessionStatus status)   {  

         Student s =  studentRep.secureFind(id); 
         if(student == null || s == null) {
             throw new ResourceNotFoundException();
         }

         if (result.hasErrors()) {  
             return "students/form";
         } 
         //student.setId(id);
         student.setSchool(profileService.getMySchool());
         studentRep.save(student);
         redirectAttributes.addFlashAttribute("message", "Επιτυχής προσθήκη!");
         status.setComplete();
         return "redirect:/students/list";  
     }  

This still leaves empty the createdBy and createdDate fields when I do an update :(

Also it does not get the School value (which is not contained in my form because it is related to the user currently editing) so I need to get it again from the SecurityContext... Have I done anything wrong ?

Update 3: For reference and to not miss it in the comments: The main problem was that I needed to include the @SessionAttributes annotation to my controller.

Warden answered 30/9, 2013 at 9:3 Comment(3)
Have you also configured spring data jpa correctly (please post the configuration), and also what is your AuditorAware implementation look like. Are you updating the object from the database or are you simply merging an entity created from a web-form (which doesn't have the created* and lastModified* fields).Husha
I made some updates to the post - your comment on merging from the web form gave me the idea to copy the createdBy and Date from the database object. Please add an answer so I can accept it :)Warden
You shouldn't be copying around those fields, you should let Springs databinding handle that for you, just like the validation etc.. See my answer.Husha
H
3

Your method in your (@)Controller class is not that efficient. You don't want to (manually) retrieve the object and copy all the fields, relationships etc. over to it. Next to that with complex objects you will sooner or alter run into big trouble.

What you want is on your first method (the GET for showing the form) retrieve the user and store it in the session using @SessionAttributes. Next you want an @InitBinder annotated method to set your validator on the WebDataBinder so that spring will do the validation. This will leave your updateFromForm method nice and clean.

@Controller
@RequestMapping("/edit/{id}")
@SessionAttributes("student")
public EditStudentController

    @Autowired  
    private StudentFormValidator validator;

    @Autowired
    private StudentRepository studentRep;

    @InitBinder
    public void initBinder(WebDataBinder binder) {
        binder.setValidator(validator);
    }

    @RequestMapping(method=RequestMethod.GET)
    public String showUpdateForm(Model model) {
        model.addObject("student", studentRep.secureFind(id));
        return "students/form";
    }

    @RequestMapping(method=RequestMethod.POST)
    public String public String updateFromForm(@Valid @ModelAttribute Student student, BindingResult result, RedirectAttributes redirectAttributes, SessionStatus status)   {  
        // Optionally you could check the ids if they are the same.
        if (result.hasErrors()) {  
            return "students/form";
        } 
        redirectAttributes.addFlashAttribute("message", "?p?t???? p??s????!");
        studentRep.save(student);
        status.setComplete(); // Will remove the student from the session
        return "redirect:/students/list";  
    }
}  

You will need to add the SessionStatus attribute to the method and mark the processing complete, so that Spring can cleanup your model from the session.

This way you don't have to copy around objects, etc. and Spring will do all the heave lifting and all your fields/relations will be properly set.

Husha answered 30/9, 2013 at 10:3 Comment(4)
Then you must be doing something wrong. Post your modified controller and your service code.Husha
You haven't modified the controller as I suggested. You are still manually retrieving and copying fields. My controller is a lot simpler then yours. Basically stop copying fields and let spring do that for you. Make sure that you have the @SessionAttributes annotation on your controller.Husha
Yes ! The problem was that I'd forgotten the @SessionAttributes :) Thank you !Warden
Hi @M.Deinum , the question is not relevant to this but I want to see is there any way I can make my code cleaner. based on your comments "you don't want to (manually) retrieve the object and copy all the fields, relationships etc.", so I am using restful instead of session, so is there anyway that I can do the same without manually copying the updating request obj?Ameeameer
R
28

Use updatable attribute of @Column annotation like below.

@Column(name = "created_date", updatable = false)
private Date createdDate;

This will retain the created date on update operation.

Rawlins answered 4/12, 2017 at 22:48 Comment(1)
This is the answer to this question.Tenedos
H
3

Your method in your (@)Controller class is not that efficient. You don't want to (manually) retrieve the object and copy all the fields, relationships etc. over to it. Next to that with complex objects you will sooner or alter run into big trouble.

What you want is on your first method (the GET for showing the form) retrieve the user and store it in the session using @SessionAttributes. Next you want an @InitBinder annotated method to set your validator on the WebDataBinder so that spring will do the validation. This will leave your updateFromForm method nice and clean.

@Controller
@RequestMapping("/edit/{id}")
@SessionAttributes("student")
public EditStudentController

    @Autowired  
    private StudentFormValidator validator;

    @Autowired
    private StudentRepository studentRep;

    @InitBinder
    public void initBinder(WebDataBinder binder) {
        binder.setValidator(validator);
    }

    @RequestMapping(method=RequestMethod.GET)
    public String showUpdateForm(Model model) {
        model.addObject("student", studentRep.secureFind(id));
        return "students/form";
    }

    @RequestMapping(method=RequestMethod.POST)
    public String public String updateFromForm(@Valid @ModelAttribute Student student, BindingResult result, RedirectAttributes redirectAttributes, SessionStatus status)   {  
        // Optionally you could check the ids if they are the same.
        if (result.hasErrors()) {  
            return "students/form";
        } 
        redirectAttributes.addFlashAttribute("message", "?p?t???? p??s????!");
        studentRep.save(student);
        status.setComplete(); // Will remove the student from the session
        return "redirect:/students/list";  
    }
}  

You will need to add the SessionStatus attribute to the method and mark the processing complete, so that Spring can cleanup your model from the session.

This way you don't have to copy around objects, etc. and Spring will do all the heave lifting and all your fields/relations will be properly set.

Husha answered 30/9, 2013 at 10:3 Comment(4)
Then you must be doing something wrong. Post your modified controller and your service code.Husha
You haven't modified the controller as I suggested. You are still manually retrieving and copying fields. My controller is a lot simpler then yours. Basically stop copying fields and let spring do that for you. Make sure that you have the @SessionAttributes annotation on your controller.Husha
Yes ! The problem was that I'd forgotten the @SessionAttributes :) Thank you !Warden
Hi @M.Deinum , the question is not relevant to this but I want to see is there any way I can make my code cleaner. based on your comments "you don't want to (manually) retrieve the object and copy all the fields, relationships etc.", so I am using restful instead of session, so is there anyway that I can do the same without manually copying the updating request obj?Ameeameer
H
0

In my case @CreatedDate and @CreatedBy fields were not removed from databse during update, but were not queried by @Repository findOne method. Changing

@Cache(usage = CacheConcurrencyStrategy.READ_WRITE)

into

@Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE)

on @Entity class helped with that.

Halfhardy answered 13/2, 2021 at 7:29 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.