Reputation: 405
fun main(args: Array<String>)
{
runApplication<JpaTest>(*args).getBean(JpaTest::class.java).test()
}
@SpringBootApplication
class JpaTest
{
@Autowired
lateinit var repository: PersonRepository
fun test()
{
repository.save(Person())
runBlocking {
suspend fun update(name: String, delay: Long)
{
val p = repository.findById(1).get()
delay(delay)
println("=== $name")
p.name = name
repository.save(p)
}
withContext(Dispatchers.Default) {
awaitAll(
async { update("Right Name", 3000) },
async { update("Wrong Name", 5000) }
)
println(" ")
val p = repository.findById(1).get()
println("Final: $p")
println(" ")
}
}
exitProcess(0)
}
}
@Repository
interface PersonRepository : CrudRepository<Person, Long>
@Entity
@DynamicUpdate
@OptimisticLocking(type=OptimisticLockType.DIRTY)
data class Person(
@Id
@GeneratedValue
val id: Long = 0,
var name: String = "?",
)
I'm trying to simulate a multi-threaded environment (or multiple instances of the same app running) where an entity is fetch from the database at the same time, changed and persisted.
When the first save task runs (3s later):
=== Right Name
Hibernate: select person0_.id as id1_0_0_, person0_.name as name2_0_0_ from person person0_ where person0_.id=?
2021-03-24 09:18:38.565 TRACE 21791 --- [atcher-worker-2] o.h.type.descriptor.sql.BasicBinder : binding parameter [1] as [BIGINT] - [1]
2021-03-24 09:18:38.566 TRACE 21791 --- [atcher-worker-2] o.h.type.descriptor.sql.BasicExtractor : extracted value ([name2_0_0_] : [VARCHAR]) - [UNKNOWN]
Hibernate: update person set name=? where id=? and name=?
2021-03-24 09:18:38.567 TRACE 21791 --- [atcher-worker-2] o.h.type.descriptor.sql.BasicBinder : binding parameter [1] as [VARCHAR] - [Right Name]
2021-03-24 09:18:38.568 TRACE 21791 --- [atcher-worker-2] o.h.type.descriptor.sql.BasicBinder : binding parameter [2] as [BIGINT] - [1]
2021-03-24 09:18:38.568 TRACE 21791 --- [atcher-worker-2] o.h.type.descriptor.sql.BasicBinder : binding parameter [3] as [VARCHAR] - [UNKNOWN]
When the second save task runs (5s later):
=== Wrong Name
Hibernate: select person0_.id as id1_0_0_, person0_.name as name2_0_0_ from person person0_ where person0_.id=?
2021-03-24 09:18:40.556 TRACE 21791 --- [atcher-worker-2] o.h.type.descriptor.sql.BasicBinder : binding parameter [1] as [BIGINT] - [1]
2021-03-24 09:18:40.556 TRACE 21791 --- [atcher-worker-2] o.h.type.descriptor.sql.BasicExtractor : extracted value ([name2_0_0_] : [VARCHAR]) - [Right Name]
Hibernate: update person set name=? where id=? and name=?
2021-03-24 09:18:40.557 TRACE 21791 --- [atcher-worker-2] o.h.type.descriptor.sql.BasicBinder : binding parameter [1] as [VARCHAR] - [Wrong Name]
2021-03-24 09:18:40.557 TRACE 21791 --- [atcher-worker-2] o.h.type.descriptor.sql.BasicBinder : binding parameter [2] as [BIGINT] - [1]
2021-03-24 09:18:40.557 TRACE 21791 --- [atcher-worker-2] o.h.type.descriptor.sql.BasicBinder : binding parameter [3] as [VARCHAR] - [Right Name]
Please note that both tasks fetched the database for the same object and then slept for 3s/5s before updating/saving.
The issue here is that the second task
fetched the database again before updating, meaning that the first task
changes are lost. I tried adding @SelectBeforeUpdate(false)
but it also didn't work.
I was expecting that when second task
attempted to persist the entity, an OptimisticLockException
would be thrown.
If I change the lockType to VERSION
it works as expected and second task
's update is rejected. Why is that?
Upvotes: 0
Views: 708
Reputation: 81907
You don't have any transaction boundaries declared.
As a result of this every call to a repository runs in its own transaction.
And this is why the entity gets loaded (again) as part of the save
operation.
If you wrap the load, wait, save cycle into a single transaction you'll see your desired behaviour.
In your test you can do that using a TransactionTemplate
for which the usage is described in the Spring Reference Documentation.
Regarding the follow up questions:
Any ideas of why
VERSION
works?
I don't have prove of the following, but this is what I strongly suspect.
With DIRTY
the original state of all the columns is stored in the session, therefor it is the state of the load operation within the current transaction.
With VERSION
the original state is part of the entity and therefore it survives across transaction boundaries.
If I have to wrap everything since load into a transaction, wouldn’t that be a bad practice depending on what I’m doing between load and save?
While I agree that transactions should be as short as possible, they shouldn't be shorter than they need to be in order to do their job. I wouldn't expect transactions of a few seconds to be problematic as long as you don't have any blocking locks.
If you want to point at a bad practice, i.e. one that needs a good explanation why you use it, that would probably using OptimisticLockType.DIRTY
. There is a reason why OptimisticLockType.VERSION
is recommended and the default.
Upvotes: 3