-
Notifications
You must be signed in to change notification settings - Fork 0
Keep front-desk owner records fresh with visible change history #2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| /* | ||
| * Copyright 2012-2025 the original author or authors. | ||
| * | ||
| * Licensed under the Apache License, Version 2.0 (the "License"); | ||
| * you may not use this file except in compliance with the License. | ||
| * You may obtain a copy of the License at | ||
| * | ||
| * https://www.apache.org/licenses/LICENSE-2.0 | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, software | ||
| * distributed under the License is distributed on an "AS IS" BASIS, | ||
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| * See the License for the specific language governing permissions and | ||
| * limitations under the License. | ||
| */ | ||
| package org.springframework.samples.petclinic.owner; | ||
|
|
||
| import java.time.LocalDateTime; | ||
|
|
||
| /** | ||
| * A staff-visible change made to an owner record. | ||
| * | ||
| * @param changeType the category of change | ||
| * @param summary a short description for the activity feed | ||
| * @param changedAt when the change was recorded | ||
| */ | ||
| public record OwnerChange(String changeType, String summary, LocalDateTime changedAt) { | ||
|
|
||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,85 @@ | ||
| /* | ||
| * Copyright 2012-2025 the original author or authors. | ||
| * | ||
| * Licensed under the Apache License, Version 2.0 (the "License"); | ||
| * you may not use this file except in compliance with the License. | ||
| * You may obtain a copy of the License at | ||
| * | ||
| * https://www.apache.org/licenses/LICENSE-2.0 | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, software | ||
| * distributed under the License is distributed on an "AS IS" BASIS, | ||
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| * See the License for the specific language governing permissions and | ||
| * limitations under the License. | ||
| */ | ||
| package org.springframework.samples.petclinic.owner; | ||
|
|
||
| import java.sql.Timestamp; | ||
| import java.time.LocalDateTime; | ||
| import java.util.List; | ||
|
|
||
| import org.springframework.cache.CacheManager; | ||
| import org.springframework.jdbc.core.JdbcTemplate; | ||
| import org.springframework.samples.petclinic.system.CacheConfiguration; | ||
| import org.springframework.stereotype.Service; | ||
|
|
||
| /** | ||
| * Records the activity feed shown to front-desk staff and refreshes cached owner search | ||
| * projections after mutations. | ||
| */ | ||
| @Service | ||
| public class OwnerChangeTracker { | ||
|
|
||
| private final JdbcTemplate jdbcTemplate; | ||
|
|
||
| private final CacheConfiguration cacheConfiguration; | ||
|
|
||
| private final CacheManager cacheManager; | ||
|
|
||
| public OwnerChangeTracker(JdbcTemplate jdbcTemplate, CacheConfiguration cacheConfiguration, | ||
| CacheManager cacheManager) { | ||
| this.jdbcTemplate = jdbcTemplate; | ||
| this.cacheConfiguration = cacheConfiguration; | ||
| this.cacheManager = cacheManager; | ||
| } | ||
|
|
||
| public void ownerCreated(Owner owner) { | ||
| recordChange(owner.getId(), "OWNER_CREATED", "Owner record created"); | ||
| } | ||
|
|
||
| public void ownerUpdated(Owner owner) { | ||
| recordChange(owner.getId(), "OWNER_UPDATED", "Contact details updated"); | ||
| } | ||
|
|
||
| public void petAdded(Owner owner, Pet pet) { | ||
| recordChange(owner.getId(), "PET_ADDED", "Pet added: " + pet.getName()); | ||
| } | ||
|
|
||
| public void petUpdated(Owner owner, Pet pet) { | ||
| recordChange(owner.getId(), "PET_UPDATED", "Pet details updated: " + pet.getName()); | ||
| } | ||
|
|
||
| public void visitBooked(Owner owner, Visit visit) { | ||
| recordChange(owner.getId(), "VISIT_BOOKED", "Visit booked: " + visit.getDescription()); | ||
| } | ||
|
|
||
| public List<OwnerChange> changesFor(int ownerId) { | ||
| return this.jdbcTemplate.query(""" | ||
| select change_type, summary, changed_at | ||
| from owner_changes | ||
| where owner_id = ? | ||
| order by changed_at desc, id desc | ||
| """, (rs, rowNum) -> new OwnerChange(rs.getString("change_type"), rs.getString("summary"), | ||
| rs.getTimestamp("changed_at").toLocalDateTime()), ownerId); | ||
| } | ||
|
|
||
| private void recordChange(Integer ownerId, String changeType, String summary) { | ||
| LocalDateTime changedAt = LocalDateTime.now(); | ||
| this.jdbcTemplate.update( | ||
| "insert into owner_changes (owner_id, change_type, summary, changed_at) values (?, ?, ?, ?)", ownerId, | ||
| changeType, summary, Timestamp.valueOf(changedAt)); | ||
| this.cacheConfiguration.refreshOwnerData(this.cacheManager); | ||
| } | ||
|
|
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -52,8 +52,11 @@ class OwnerController { | |
|
|
||
| private final OwnerRepository owners; | ||
|
|
||
| public OwnerController(OwnerRepository owners) { | ||
| private final OwnerChangeTracker changeTracker; | ||
|
|
||
| public OwnerController(OwnerRepository owners, OwnerChangeTracker changeTracker) { | ||
| this.owners = owners; | ||
| this.changeTracker = changeTracker; | ||
| } | ||
|
|
||
| @InitBinder | ||
|
|
@@ -82,6 +85,7 @@ public String processCreationForm(@Valid Owner owner, BindingResult result, Redi | |
| } | ||
|
|
||
| this.owners.save(owner); | ||
| this.changeTracker.ownerCreated(owner); | ||
| redirectAttributes.addFlashAttribute("message", "New Owner Created"); | ||
| return "redirect:/owners/" + owner.getId(); | ||
| } | ||
|
|
@@ -157,6 +161,7 @@ public String processUpdateOwnerForm(@Valid Owner owner, BindingResult result, @ | |
|
|
||
| owner.setId(ownerId); | ||
| this.owners.save(owner); | ||
| this.changeTracker.ownerUpdated(owner); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When staff submit the owner edit form without changing any values, this unconditional call still appends an Useful? React with 👍 / 👎. |
||
| redirectAttributes.addFlashAttribute("message", "Owner Values Updated"); | ||
| return "redirect:/owners/{ownerId}"; | ||
| } | ||
|
|
@@ -173,6 +178,7 @@ public ModelAndView showOwner(@PathVariable("ownerId") int ownerId) { | |
| Owner owner = optionalOwner.orElseThrow(() -> new IllegalArgumentException( | ||
| "Owner not found with id: " + ownerId + ". Please ensure the ID is correct ")); | ||
| mav.addObject(owner); | ||
| mav.addObject("ownerChanges", this.changeTracker.changesFor(ownerId)); | ||
| return mav; | ||
| } | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -17,6 +17,7 @@ | |
|
|
||
| import java.util.Optional; | ||
|
|
||
| import org.springframework.cache.annotation.Cacheable; | ||
| import org.springframework.data.domain.Page; | ||
| import org.springframework.data.domain.Pageable; | ||
| import org.springframework.data.jpa.repository.JpaRepository; | ||
|
|
@@ -42,6 +43,7 @@ public interface OwnerRepository extends JpaRepository<Owner, Integer> { | |
| * @return a Collection of matching {@link Owner}s (or an empty Collection if none | ||
| * found) | ||
| */ | ||
| @Cacheable("ownerSearch") | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When a cache-miss search overlaps an owner mutation, the search can read the pre-mutation data, the mutation can commit and clear Useful? React with 👍 / 👎. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Every distinct user-controlled last-name and page combination passed to Useful? React with 👍 / 👎. |
||
| Page<Owner> findByLastNameStartingWith(String lastName, Pageable pageable); | ||
|
|
||
| /** | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When a visit description is between 242 and 255 characters, saving the visit succeeds in H2/MySQL because
visits.descriptionisVARCHAR(255), but adding theVisit booked:prefix makes this audit value exceedowner_changes.summary VARCHAR(255). The subsequent insert then fails, returning a 500 after the visit has already been committed and leaving no history entry; widen the summary column or truncate/validate the generated summary.Useful? React with 👍 / 👎.