-
Notifications
You must be signed in to change notification settings - Fork 695
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
feat!: EXPOSED-109 change implement of spring transaction manager #1840
feat!: EXPOSED-109 change implement of spring transaction manager #1840
Conversation
Oh, I found it |
@FullOfOrange Thanks for putting a PR together so quickly, it's looking good so far! I'll checkout the branch more thoroughly over the next coming days, but two general questions from a glance:
|
First, It's my mistakes.. :) i'll add default value Second, I wrote it to work as well as possible with the current test version, and the existing tests cover the test cases I expect. However, there are a few expected issues
I've written an implementation as quickly as I could to get your thoughts. I'll try to write the expected tests, such as rollbacks, after I confirm this implementation direction. |
@bog-walk It's been a while! I've tried a few things, but the current implementation seems to be the best. So I wrote some test code to see if SpringTransactionManager properly sets up a TransactionManager. I referenced this link for how to write the test code, and used the TransactionTemplate and mockk to test that the connection was managed well. I didn't write tests for Propagation because it is not supported in previous versions. We will add tests in PRs that implement these features in the future. If you think there are other tests that should be included, or if you need to write a test for Propagation support, please comment. |
spring-transaction/src/test/kotlin/org/jetbrains/exposed/spring/SpringTransactionManagerTest.kt
Outdated
Show resolved
Hide resolved
...nsaction/src/test/kotlin/org/jetbrains/exposed/spring/SpringMultiContainerTransactionTest.kt
Outdated
Show resolved
Hide resolved
...nsaction/src/test/kotlin/org/jetbrains/exposed/spring/SpringMultiContainerTransactionTest.kt
Outdated
Show resolved
Hide resolved
...nsaction/src/test/kotlin/org/jetbrains/exposed/spring/SpringMultiContainerTransactionTest.kt
Outdated
Show resolved
Hide resolved
...nsaction/src/test/kotlin/org/jetbrains/exposed/spring/SpringMultiContainerTransactionTest.kt
Outdated
Show resolved
Hide resolved
...nsaction/src/test/kotlin/org/jetbrains/exposed/spring/SpringMultiContainerTransactionTest.kt
Outdated
Show resolved
Hide resolved
spring-transaction/src/main/kotlin/org/jetbrains/exposed/spring/SpringTransactionManager.kt
Show resolved
Hide resolved
spring-transaction/src/main/kotlin/org/jetbrains/exposed/spring/SpringTransactionManager.kt
Outdated
Show resolved
Hide resolved
spring-transaction/src/main/kotlin/org/jetbrains/exposed/spring/SpringTransactionManager.kt
Outdated
Show resolved
Hide resolved
) : DataSourceTransactionManager(dataSource), TransactionManager { | ||
) : AbstractPlatformTransactionManager() { |
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.
A question about breaking changes and migration for users since TransactionManager
interface will no longer be implemented:
- All interface functions will no longer be callable on a
SpringTransactionManager
instance, namelycurrentOrNull()
andnewTransaction()
:- For example, the user in this issue uses
newTransaction()
to set a non-default isolation level. Is the plan that this use would be replaced with@Transactional(isolation = ?)
? - And would it be possible to return the current transaction, by the user going through the database, like
tm.database.transactionManager.currentOrNull()
? Or is the intention that this implementation delegates all transaction operations to Spring, so the user should not need to call these functions anymore?
- For example, the user in this issue uses
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.
I didn't realize there was a case where a user could create and use a new instance object that wasn't a bean.
If used in this way, I think it's possible that Spring's transaction management and Exposed's transaction management could get mixed up, making it difficult to manage connections.
-
I would prefer not to use newTransaction and currentOrNull in a SpringTransactionManager instance in this situation, and if you agree, it would be a good idea to implement a TransactionManager that throws an error if newTransaction and currentOrNull are used.
-
this following code provides an isolation level by using @transactional(isolation = ?), but I haven't verified that this properly implements Spring's policy. I'll try to implement Isolation and Propagation as per Spring's policy in the following PR.
override fun doBegin(transaction: Any, definition: TransactionDefinition) {
val trxObject = transaction as ExposedTransactionObject
val currentTransactionManager = trxObject.manager
TransactionManager.resetCurrent(currentTransactionManager)
currentTransactionManager.currentOrNull()
?: currentTransactionManager.newTransaction(
isolation = definition.isolationLevel,
readOnly = definition.isReadOnly,
).apply {
if (showSql) {
addLogger(StdOutSqlLogger)
}
}
}
- I exposed the database to check if the TransactionManager is set up properly when testing, but if there is another way to check the database at test time, I will prevent the database instance from being accessed with tm.database. Can you give me any advice?
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.
- For the next release, the loss of
TransactionManager
functions would be mentioned as a breaking change and I'm drafting a migration section to make clear any changes. For now we'll see what issues pop up for example if users choose to call a new transaction within aSpringTransactionManager
instance in spite of our suggestions. - Sounds good to me.
- Our core unit tests usually involve passing in the database directly so it is available as a test property. But in special cases, it can be checked via
TransactionManager.currentOrNull()?.db
(see MultidatabaseTest.kt for an example). I ran your tests using the following assertion and they all passed so it could be a valid replacement fortm.database
:
assertEquals(
TransactionManager.managerFor(this.database),
TransactionManager.managerFor(TransactionManager.currentOrNull()?.db)
)
assertEquals(
TransactionManager.managerFor(TransactionManager.currentOrNull()?.db),
TransactionManager.manager
)
Note that the tests that pre-assign a variable would need to call this database check instead only when needed:
val tm = SpringTransactionManager(ds1)
val tm2 = SpringTransactionManager(ds2)
// val transactionManager2 = TransactionManager.managerFor(tm2.database)
tm2.executeAssert(false) {
tm.executeAssert(false)
assertEquals(TransactionManager.managerFor(TransactionManager.currentOrNull()?.db), TransactionManager.manager)
}
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.
All interface functions will no longer be callable on a SpringTransactionManager instance, namely currentOrNull() and newTransaction():
And would it be possible to return the current transaction, by the user going through the database, like tm.database.transactionManager.currentOrNull()? Or is the intention that this implementation delegates all transaction operations to Spring, so the user should not need to call these functions anymore?
I changed the database instance to be inaccessible directly, and the transaction is now only accessible via TransactionManager.currentOrNull().
My intention is, as you say, that the user should not be able to use the Exposed Transaction directly when using the PlatformTransactionManager Bean.
However, even if the user accesses the transaction with TransactionManager.currentOrNull(), TransactionManager.currentOrNew() it will behave almost identically to the transaction { } in Exposed, so as long as user don't call transaction.close(), it shouldn't have any problems.
The recommendation is to avoid calling the TransactionManager directly.
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.
Thanks for all the effort you've put into this PR so far! These should be the last change requests from me. Please let me know if you need any help/clarification with the mockk
dependency request.
spring-transaction/src/main/kotlin/org/jetbrains/exposed/spring/SpringTransactionManager.kt
Outdated
Show resolved
Hide resolved
@bog-walk Thank you so much for the detailed review of PR! I've fixed most of the things you mentioned. I would be very grateful if you could check PR one more time. Please leave a comment if you have any other issues you'd like to discuss, or anything else you'd like to see remove implementation of the TransactionManage. If you don't want to make any breaking changes, I'll try to find out with a new implementation. |
} | ||
} | ||
|
||
class ConnectionSpy : Connection { |
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.
There's quite a lot of overrides when implementing Connection
from scratch.
I realize that the goal of this test suite is to mock transaction and connection calls without relying on an actual database or actual returned results. Would the validity or isolation of these unit tests be significantly compromised if the data source at least started with a base connection from the H2 driver? Especially if all the function calls being tested are overriden in ConnectionSpy
?
Please think about it and consider the following alternative:
class ConnectionSpy(private val connection: Connection) : Connection by connection {
// then remove all overrides not implemented
}
class DataSourceSpy(private val connectionSpy: (Connection) -> Connection) : DataSource {
// ...
override fun getConnection() = connection ?: run {
val testConnection = DriverManager.getConnection("jdbc:h2:mem:test")
connectionSpy(testConnection)
}
}
private val ds1 = DataSourceSpy(::ConnectionSpy)
private val con1 = ConnectionSpy(ds1.connection)
// `DriverManager.getConnection()` would only be invoked once on this first access
// then the value would be accessed from the private variable `connection`
// which can continue to be stored using `mockConnection()`
// or by adding an assignment in the null branch of `getConnection()` above.
Otherwise maybe the 2 spy classes need to at least be extracted to their own file and I'll get a second opinion again on what our options are.
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.
I used the mock object to test the behavior of the connection object and if there is another way to do it, I will use it. If it makes sense to test whether or not the method is called in the way you suggested using the example code above, I would change it to do so.
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.
private open class DataSourceSpy(private val connectionSpy: (Connection) -> Connection) : DataSource {
var con: Connection = connectionSpy(DriverManager.getConnection("jdbc:h2:mem:test")
override fun getConnection() = con
// other override functions that throw not implement exception
}
private open class ConnectionSpy(private val connection: Connection) : Connection by connection {
var commitCallCount: Int = 0
...
// mock functions that need to test
}
I changed it to use the H2 connection implementation as you suggested.
Since the purpose is to check whether the method is called or not, I think this should be able to complete the test without any problems. If you think there is a better way to do this, or if you think I should move the Spy class to a different file, please comment.
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.
lgtm
} | ||
} | ||
|
||
private open class DataSourceSpy(private val connectionSpy: (Connection) -> Connection) : DataSource { |
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.
please consider extracting utility classes to a separate with internal modifier
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.
@e5l Just to clarify your question
- move the spy classes outside of the test class and then change them to internal classes, 2. just change them to internal classes, or 3. move them to a new file with internal modifier?
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.
a new file is preferred
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.
I created a file called TestSpys.kt that contains a collection of spy classes.
I think it's a bit of a bad name, but I'll change it if there's a convention or a recommendation.
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.
I would name it same as a class by default
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.
@FullOfOrange Just to clarify above, feel free to create 2 new files, each with its own internal class, named after the class. So 1 for DataSourceSpy
and 1 for ConnectionSpy
.
Also, there should be no conflict, but please consider doing a rebase
from main, which has recently had some changes.
9984f09
to
c0c348b
Compare
…tBrains#1840) * refactor: Implement a new one with AbstractPlatformTransactionManager * refactor: remove unused property * fix: remove nullable * fix: add show sql feature * fix: rebuild api file * chore: remove property * chore: add default show sql value * fix: api definition * feat: add default database config value * feat :Add SmartTranscationObject implementation * feat: close connection when transaction end * chore: remove unused values * feat: reset outer tx manager when transaction clean up * test: Add outer transaction manager setting exception test * chore: Add import * test: add manager setting test * chore: remake api file * fix: fix when commit or rollback failure * chore: fix detekt issue * test: Add transactionAwareDataSourceProxy test * chore: refactor spring transaction manager test * refactor: remove default value in test * refactor: apply code review * refactor: apply test code review * refactor: merge duplicate tests * refactor: remove mockk and change exposed transaction object to private * refactor: move database to private * refactor: clean up transaction manager after test ended * fix: while condition * test: Add exposed and spring combine transaction * fix: test * fix: api dump * refactor: apply connection spy suggestion * refactor: apply code review * refactor: apply code reivew
I implemented all the necessary override functions and tried to have as much functionality as the current SpringTransactionManager has.
transaction { } method to set the TransactionManager, I used the same approach for Spring's transactions.
At doBegin(), we check for the existence of an external transaction manager and create a new transaction.
At doCommit() and doRollback(), we finalize the transaction and either return to the outer transaction or set the TransactionManager to null.
The second approach, which is still in the works, is to implement two new interfaces, TransactionManager and TransactionInterface from Exposed, and use them for Spring's transactions. I think this approach will be much more difficult to maintain than ThreadLocalTransactionManager, so I'm hoping that the first approach will be adopted
Sorry for my poor english, I hope my opinion is clear.
Thanks for your review