We’ve added some entities in previous articles, and made them belong to users. Most notably, those entities have been bank accounts. That’s great, as users can now enter the balances of their accounts already in our application, as opposed to that silly Excel sheet. However, these balances hardly ever remain constant, as there is usually some movement.
Now, we could simply add an update functionality to our application, where the user can enter the new balance, and he’ll always know how much he’s got, right? Sure, that would work. However, it’d be even nicer if he could actually see the movement of the account over time, to track whether the total is going up, or going down.
Existing Libraries
There are some great, helpful libraries out there that could help us with this. One such library is envers, which is a hibernate plugin and works great on relational databases. I would argue though that envers is not quite what we require, as it is usually used in auditing. We do not require a thorough auditing in our application, as the user can add whatever he’d like. So, we’ll simply create our custom endpoints and functionality to reach an outcome that’s tailored to our needs.
The historical snapshot
Since we’re thinking about what we’d actually like to store first, let’s note the precise information that we’d need.
- the balance
- the account it’s a snapshot of
- the validity timeframe
That’s really already everything that it needs! It doesn’t require the name of the account, as that could change, and has no relevance to this feature. It also doesn’t need the information of the user it belongs to, as we have that information transiently through the account itself. Also, the user will not get access to the history, unless the account actually belongs to him.
That being said, here’s the AccountSnapshot
entity:
@Entity
@Table(name = "account_snapshot")
data class AccountSnapshot(
@Id
@GeneratedValue(generator = "UUID")
@GenericGenerator(name = "UUID", strategy = "org.hibernate.id.UUIDGenerator")
val id: UUID? = null,
@Column(name = "balance", nullable = false) val balance: Double,
@Column(name = "valid_from", nullable = false) val validFrom: LocalDate,
@Column(name = "valid_to", nullable = false) val validTo: LocalDate = LocalDate.now(),
@ManyToOne(fetch = FetchType.LAZY) val account: Account
)
Most of it seems pretty self-explanatory. However, some clarifications about the timeline need to be mentioned:
- validTo is always the day itself, as this is when a new value is added
- validFrom is based on the last Update of the account
- the editedOn column needs to be added to the account, as this will serve as the entry for the validFrom in future updates
Next, we have its corresponding entry in the Account
:
@Column(name = "edited_on", nullable = false) val editedOn: LocalDate = LocalDate.now()
@OneToMany(cascade = [CascadeType.ALL], orphanRemoval = true, mappedBy = "account")
val accountSnapshots: MutableList<AccountSnapshot> = mutableListOf()
Similarly, we have the DTO changes:
data class AccountSnapshotDto(
val id: UUID,
val balance: Double,
val validFrom: LocalDate,
val validTo: LocalDate = LocalDate.now()
)
data class AccountDto(
...
@JsonInclude(JsonInclude.Include.NON_EMPTY)
val snapshots: List<AccountSnapshotDto> = listOf()
)
data class BalanceDto(val balance: Double)
Adding the functionality
Great, now we have the domain and dto models all sorted out. Next, we will add the code that will enable the functionality.
Domain object layer
The first layer is logically the Service layer, where changes will happen. However, since I’m a big fan of having the Service
mostly be a guide on how to orchestrate functionality, most of the logic is done in the classes itself. So, we need the
following operations in the Account
:
- Create the Snapshot
- Update the balance
Creating the snapshot
This is quite easy, as we have should all the information we need to create a shiny new snapshot: `
fun createSnapshot(): AccountSnapshot {
return AccountSnapshot(
balance = this.balance,
validFrom = editedOn,
account = this
)
}
Updating the balance
Now, changing the balance is just a little bit more complicated, since the balance
is a value, and can therefore not be
changed. So, we create a new account with all the same values, except for the balance. This is a particularity of val
in
kotlin, as in Java, one could simply have made a Setter for the object. In Kotlin, we could technically have done it the same
way by declaring the balance
with var
, but then we could never be sure that this wouldn’t be used in other places also.
fun updateBalance(balance: Double): Account {
return this.copy(balance = balance, editedOn = LocalDate.now())
}
Service layer
Great, now we add the functionality in the Service layer, where the operations look very clean and in order:
@Transactional
fun updateAccount(accountId: UUID, balance: Double) {
val account = getAccount(accountId)
val snapshot = account.createSnapshot()
account.accountSnapshots.add(snapshot)
val updatedAccount = account.updateBalance(balance)
accountRepository.save(updatedAccount)
}
For hibernate, it will simply look as though the object has been changed, and it will only perform the changes on that account - not create a new one.
Controller layer
Finally, we expose the endpoint:
@PostMapping("$ACCOUNT_BY_ID/update")
fun updateAccount(@PathVariable(ACCOUNT_ID) accountId: UUID, @RequestBody balanceDto: BalanceDto): ResponseEntity<Nothing> {
accountService.updateAccount(accountId, balanceDto.balance)
return ResponseEntity.noContent().build()
}
This is again very simple. The only part I’d like to point out is that it is a POST
operation, not a PUT
. This is deliberate,
as updating the value will create a new item in a database, and not simply update an existing one. It is therefore
not idempotent, as a PUT
would be.
Testing
After changing the Postman suite we have, and of course the unit tests, both of which I am not going to add here (as they’ve
been covered in multiple places throughout other articles), we can see the following response upon a GET
after updating
the balance. As expected, it contains a new balance, but it has the history in it also!
Entity editing
While the next part is not technically part of the historization, it is important also for entity changes.
Typically, an account requires a balance update regularly. The user may want to update his account in different other ways also, though. He may have chosen a name that is not quite what he wants anymore (e.g. SalaryAccount which becomes MainAccount). Or he may want to change the currency, or the description. Whatever the case may be, we should offer it also.
One important item to stress is that here, we do not create a historical entry. The user could technically update his balance in the same operation, but that’s not what this endpoint should be used for. If the user does it, that’s on him. Of course, this should be clearly reflected in the UI that would trigger these endpoints (for example with information bubbles next to both buttons).
Here are the changes, in a similar structure to before.
Domain layer
fun setImmutableInfoFromAccount(account: Account): Account {
return this.copy(
id = account.id,
accountSnapshots = account.accountSnapshots,
addedOn = account.addedOn,
user = account.user
)
}
You may note there are quite some custom settings here. It would have been fine to do this the other way, i.e. use the existing account as the basis and copy into the new object the values from the new DTO. The reason I’ve gone this way is that it’s much more likely that new updatable attributes will be added, rather than immutable ones. Therefore, I expect this function to change less as it is now.
Service layer
@Transactional
fun editAccount(accountId: UUID, editedAccount: Account) {
val account = getAccount(accountId)
val updatedAccount = editedAccount.setImmutableInfoFromAccount(account)
accountRepository.save(updatedAccount)
}
Controller layer
@PutMapping(ACCOUNT_BY_ID)
fun editAccount(@PathVariable(ACCOUNT_ID) accountId: UUID, @RequestBody accountDto: AccountDto): ResponseEntity<Nothing> {
accountService.editAccount(accountId, accountDto.toDomain())
return ResponseEntity.noContent().build()
}
Again, testing should be added for this functionality also. As expected, the account can now also be edited (but it’s not clear from the results, as we do not see the previous input anymore). You’ll need to trust that I’ve added the appropriate assertions in the other tests.
There you have it - historization and editing for the account entities. Just one step more to add useful functionality for the user!
Feel free to let me know of other, useful features that these two operations should offer. :)