Foreign
Foreign

Reputation: 405

Issue with OptimisticLockType.DIRTY not working as expected


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

Answers (1)

Jens Schauder
Jens Schauder

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

Related Questions