0

I've got a problem, I made a CRUD in springboot with MYSQL and now I want to create a method which will return update history of my object...

I have class like:

@Entity
@Table
@EntityListeners(AuditingEntityListener.class)
@JsonIgnoreProperties(value = {"createdAt", "updatedAt"}, allowGetters = true)
@Audited
public class Note implements Serializable
{
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Getter
    @Setter
    private Long id;

    @NotBlank
    @Getter
    @Setter
    private String title;

    @Version
    @Getter
    @Setter
    private long version;

    @NotBlank
    @Getter
    @Setter
    private String content;

    @Column(nullable = false, updatable = false)
    @Temporal(TemporalType.TIMESTAMP)
    @CreatedDate
    @Getter
    @Setter
    private Date createdAt;

    @Column(nullable = false)
    @Temporal(TemporalType.TIMESTAMP)
    @LastModifiedDate
    @Getter
    @Setter
    private Date updatedAt;
}

But I don't know how can I now create a HTTP call to show that history of updates by @Audited.

I found something like this: Find max revision of each entity less than or equal to given revision with envers

But I don't know how to implement it in my project...

@RestController
@RequestMapping("/api")
public class NoteController
{
    @Autowired
    NoteRevisionService noteRevisionService;
    @Autowired
    NoteRepository noteRepository;

    // Get All Notes
    @GetMapping("/notes")
    public List<Note> getAllNotes() {
        return noteRepository.findAll();
    }

    // Create a new Note
    @PostMapping("/notes")
    public Note createNote(@Valid @RequestBody Note note) {
        return noteRepository.save(note);
    }

    // Get a Single Note
    @GetMapping("/notes/{id}")
    public Note getNoteById(@PathVariable(value = "id") Long noteId) {
        return noteRepository.findById(noteId)
                .orElseThrow(() -> new ResourceNotFoundException("Note", "id", noteId));
    }
    @GetMapping("/notes/{id}/version")
    public List<?> getVersions(@PathVariable(value = "id") Long noteId)
    {
        return noteRevisionService.getNoteUpdates(noteId);
    }
    // Update a Note
    @PutMapping("/notes/{id}")
    public Note updateNote(@PathVariable(value = "id") Long noteId,
                           @Valid @RequestBody Note noteDetails) {

        Note note = noteRepository.findById(noteId)
                .orElseThrow(() -> new ResourceNotFoundException("Note", "id", noteId));

        note.setTitle(noteDetails.getTitle());
        note.setContent(noteDetails.getContent());

        Note updatedNote = noteRepository.save(note);
        return updatedNote;
    }

    // Delete a Note
    @DeleteMapping("/notes/{id}")
    public ResponseEntity<?> deleteNote(@PathVariable(value = "id") Long noteId) {
        Note note = noteRepository.findById(noteId)
                .orElseThrow(() -> new ResourceNotFoundException("Note", "id", noteId));

        noteRepository.delete(note);

        return ResponseEntity.ok().build();
    }
}

getVersions its the call of function which Joe Doe sent me.

There: Repository

@Repository
public interface NoteRepository extends JpaRepository<Note, Long>
{
}
dontom
  • 3
  • 3

1 Answers1

0

You can use AuditQuery for this. The getNoteUpdates method below returns a list of mappings. Each mapping contains an object state and the time of the update that led to that state.

@Service
@Transactional
public class NoteRevisionService {

    private static final Logger logger = LoggerFactory.getLogger(NoteRevisionService.class);

    @PersistenceContext
    private EntityManager entityManager;

    @SuppressWarnings("unchecked")
    public List<Map.Entry<Note, Date>> getNoteUpdates(Long noteId) {
        AuditReader auditReader = AuditReaderFactory.get(entityManager);

        AuditQuery query = auditReader.createQuery()
                .forRevisionsOfEntity(Note.class, false, false)
                .add(AuditEntity.id().eq(noteId)) // if you remove this line, you'll get an update history of all Notes
                .add(AuditEntity.revisionType().eq(RevisionType.MOD)); // we're only interested in MODifications

        List<Object[]> revisions = (List<Object[]>) query.getResultList();
        List<Map.Entry<Note, Date>> results = new ArrayList<>();

        for (Object[] result : revisions) {
            Note note = (Note) result[0];
            DefaultRevisionEntity revisionEntity = (DefaultRevisionEntity) result[1];

            logger.info("The content of the note updated at {} was {}", revisionEntity.getRevisionDate(), note.getContent());
            results.add(new SimpleEntry<>(note, revisionEntity.getRevisionDate()));
        }

        return results;
    }
}

Note that if you can restrict the query somehow (for example by filtering on a property), you should definitely do it, because otherwise performing the query can have a negative impact on the performance of your entire application (the size of the returned list might be huge if this object was often updated).

Since the class has been annotated with the @Service annotation, you can inject/autowire NoteRevisionService like any other regular Spring bean, particularly in a controller that handles a GET request and delegates to that service.

UPDATE

I didn't know that extra steps had to be taken to serialize a list of map entries. There may be a better solution but the following approach gets the job done and you can customize the format of the output revisionDate with a simple annotation.

You need to define another class, say NoteUpdatePair, like so:

public class NoteUpdatePair {

    private Note note;
    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss")
    private Date revisionDate; // this field is of type java.util.Date (not java.sql.Date)

    NoteUpdatePair() {}

    public NoteUpdatePair(Note note, Date revisionDate) {
        this.note = note;
        this.revisionDate = revisionDate;
    }

    public Note getNote() {
        return note;
    }

    public void setNote(Note note) {
        this.note = note;
    }

    public Date getRevisionDate() {
        return revisionDate;
    }

    public void setRevisionDate(Date revisionDate) {
        this.revisionDate = revisionDate;
    }
}

and now, instead of returning a list of map entries, you'll return a list of NodeUpdatePair objects:

@Service
@Transactional
public class NoteRevisionService {

    private static final Logger logger = LoggerFactory.getLogger(NoteRevisionService.class);

    @PersistenceContext
    private EntityManager entityManager;

    @SuppressWarnings("unchecked")
    public List<NoteUpdatePair> getNoteUpdates(Long noteId) {
        AuditReader auditReader = AuditReaderFactory.get(entityManager);

        AuditQuery query = auditReader.createQuery()
                .forRevisionsOfEntity(Note.class, false, false)
                .add(AuditEntity.id().eq(noteId)) // if you remove this line, you'll get an update history of all Notes
                .add(AuditEntity.revisionType().eq(RevisionType.MOD)); // we're only interested in MODifications

        List<Object[]> revisions = (List<Object[]>) query.getResultList();
        List<NoteUpdatePair> results = new ArrayList<>();

        for (Object[] result : revisions) {
            Note note = (Note) result[0];
            DefaultRevisionEntity revisionEntity = (DefaultRevisionEntity) result[1];

            logger.info("The content was {}, updated at {}", note.getContent(), revisionEntity.getRevisionDate());
            results.add(new NoteUpdatePair(note, revisionEntity.getRevisionDate()));
        }

        return results;
    }
}

Regarding your question about the service's usage, I can see that you've already autowired it into your controller, so all you need to do is expose an appropriate method in your NoteController:

@RestController
@RequestMapping("/api")
public class NoteController {

    @Autowired
    private NoteRevisionService revisionService;

    /*
        the rest of your code...
     */

    @GetMapping("/notes/{noteId}/updates")
    public List<NoteUpdatePair> getNoteUpdates(@PathVariable Long noteId) {
        return revisionService.getNoteUpdates(noteId);
    }
}

Now when you send a GET request to ~/api/notes/1/updates (assuming nodeId is valid), the output should be properly serialized.

Joe Doe
  • 889
  • 8
  • 4
  • So now in my Controller where I have my CRUD I have to @Autowired this NoteRevision above and it's all? How can I now make a HTTP call in POSTMAN? I will show you my controller in post below and can you tell me if I'm doin good? – dontom Jun 24 '18 at 15:37
  • I'm a little bit new in spring and i don't understand how can I now use this function in my Get, what I have to change in my NoteRepository and NoteController? I added it to my main post. – dontom Jun 24 '18 at 15:59
  • I mean that when I'm using GET in POSTMAN it gives me a JSON like: `{title, content, created, updated}`, how can I get that with that function? I mean when I'm sending HTTP call of this function I get all versions of that in JSON like this `{title, content, created, updated}`, `{title, content, created, updated}`, `{title, content, created, updated}`. – dontom Jun 24 '18 at 16:48
  • My http call gives me answer like: `{ "com.example.mynotes.mynote.model.Note@c3877f8": "2018-06-24T13:57:28.325+0000" }, { "com.example.mynotes.mynote.model.Note@555b7e06": "2018-06-24T13:57:31.133+0000" }` – dontom Jun 24 '18 at 16:57
  • Ok, thanks you. I will check it after national match. – dontom Jun 24 '18 at 17:59
  • Thanks you! It works perfectly! I have only one question: why it doesn't show me first version, I mean that version which was created by POST call. – dontom Jun 24 '18 at 22:08
  • To include the first version in the list of results, in the service's `getNoteUpdates` method you need to replace this line: `.add(AuditEntity.revisionType().eq(RevisionType.MOD));` with this one: `.add(AuditEntity.revisionType().in(new RevisionType[]{ RevisionType.ADD, RevisionType.MOD }));` By the way, since you found my answer helpful, I would appreciate it, if you could mark it as accepted – Joe Doe Jun 24 '18 at 23:40
  • i have a question: what possibilities gives me NoteUpdatesPair? It stores this history in DB, yes? And another question how can I print in JSON revNumber? – dontom Jun 25 '18 at 12:49