JetBrains / Exposed

Kotlin SQL Framework
http://jetbrains.github.io/Exposed/
Apache License 2.0
8.25k stars 690 forks source link

One-To-Many Reference is null - But shouldn't be null #1497

Open daniel-reinhold opened 2 years ago

daniel-reinhold commented 2 years ago

I have two tables:

User:

object Users : IntIdTable("users") {
    val firstName = varchar("first_name", 64)
    val lastName = varchar("last_name", 64)
}

class UserEntity(id: EntityID<Int>) : IntEntity(id) {
    companion object : IntEntityClass<UserEntity>(Users)

    var firstName by Users.firstName
    var lastName by Users.lastName
    val permissions by UserPermissionEntity referrersOn UserPermissions.user
}

UserPermissions:

object UserPermissions : IntIdTable("user_permissions") {
    val user: Column<EntityID<Int>> = reference("user_id", Users.id)
    val permission = integer("permission")
}

class UserPermissionEntity(id: EntityID<Int>) : IntEntity(id) {
    companion object : IntEntityClass<UserPermissionEntity>(UserPermissions)

    var user by UserEntity referencedOn UserPermissions.user
    var permission by UserPermissions.permission
}

Now, when I query a User (e.G. UserEntity.findById(1)) and want to access the permissions, I get following exception:

java.lang.NullPointerException: null
    at de.todonxt.api.endpoints.user.UserEndpointsKt$userRoute$3.invokeSuspend(UserEndpoints.kt:84)
    at de.todonxt.api.endpoints.user.UserEndpointsKt$userRoute$3.invoke(UserEndpoints.kt)
    at de.todonxt.api.endpoints.user.UserEndpointsKt$userRoute$3.invoke(UserEndpoints.kt)
    at io.ktor.server.resources.RoutingKt$handle$2.invokeSuspend(Routing.kt:197)
    at io.ktor.server.resources.RoutingKt$handle$2.invoke(Routing.kt)
    at io.ktor.server.resources.RoutingKt$handle$2.invoke(Routing.kt)
    at io.ktor.server.routing.Route$buildPipeline$1$1.invokeSuspend(Route.kt:116)
    at io.ktor.server.routing.Route$buildPipeline$1$1.invoke(Route.kt)
    at io.ktor.server.routing.Route$buildPipeline$1$1.invoke(Route.kt)
    at io.ktor.util.pipeline.SuspendFunctionGun.loop(SuspendFunctionGun.kt:123)
    at io.ktor.util.pipeline.SuspendFunctionGun.proceed(SuspendFunctionGun.kt:81)
    at io.ktor.util.pipeline.SuspendFunctionGun.execute$ktor_utils(SuspendFunctionGun.kt:101)
    at io.ktor.util.pipeline.Pipeline.execute(Pipeline.kt:77)
    at io.ktor.server.routing.Routing$executeResult$$inlined$execute$1.invokeSuspend(Pipeline.kt:478)
    at io.ktor.server.routing.Routing$executeResult$$inlined$execute$1.invoke(Pipeline.kt)
    at io.ktor.server.routing.Routing$executeResult$$inlined$execute$1.invoke(Pipeline.kt)
    at io.ktor.util.debug.ContextUtilsKt.initContextInDebugMode(ContextUtils.kt:17)
    at io.ktor.server.routing.Routing.executeResult(Routing.kt:174)
    at io.ktor.server.routing.Routing.interceptor(Routing.kt:49)
    at io.ktor.server.routing.Routing$Plugin$install$1.invokeSuspend(Routing.kt:124)
    at io.ktor.server.routing.Routing$Plugin$install$1.invoke(Routing.kt)
    at io.ktor.server.routing.Routing$Plugin$install$1.invoke(Routing.kt)
    at io.ktor.util.pipeline.SuspendFunctionGun.loop(SuspendFunctionGun.kt:123)
    at io.ktor.util.pipeline.SuspendFunctionGun.proceed(SuspendFunctionGun.kt:81)
    at io.ktor.server.engine.BaseApplicationEngineKt$installDefaultTransformationChecker$1.invokeSuspend(BaseApplicationEngine.kt:122)
    at io.ktor.server.engine.BaseApplicationEngineKt$installDefaultTransformationChecker$1.invoke(BaseApplicationEngine.kt)
    at io.ktor.server.engine.BaseApplicationEngineKt$installDefaultTransformationChecker$1.invoke(BaseApplicationEngine.kt)
    at io.ktor.util.pipeline.SuspendFunctionGun.loop(SuspendFunctionGun.kt:123)
    at io.ktor.util.pipeline.SuspendFunctionGun.proceed(SuspendFunctionGun.kt:81)
    at io.ktor.server.application.hooks.CallFailed$install$1$1.invokeSuspend(CommonHooks.kt:41)
    at io.ktor.server.application.hooks.CallFailed$install$1$1.invoke(CommonHooks.kt)
    at io.ktor.server.application.hooks.CallFailed$install$1$1.invoke(CommonHooks.kt)
    at kotlinx.coroutines.intrinsics.UndispatchedKt.startUndispatchedOrReturn(Undispatched.kt:89)
    at kotlinx.coroutines.CoroutineScopeKt.coroutineScope(CoroutineScope.kt:264)
    at io.ktor.server.application.hooks.CallFailed$install$1.invokeSuspend(CommonHooks.kt:40)
    at io.ktor.server.application.hooks.CallFailed$install$1.invoke(CommonHooks.kt)
    at io.ktor.server.application.hooks.CallFailed$install$1.invoke(CommonHooks.kt)
    at io.ktor.util.pipeline.SuspendFunctionGun.loop(SuspendFunctionGun.kt:123)
    at io.ktor.util.pipeline.SuspendFunctionGun.proceed(SuspendFunctionGun.kt:81)
    at io.ktor.util.pipeline.SuspendFunctionGun.execute$ktor_utils(SuspendFunctionGun.kt:101)
    at io.ktor.util.pipeline.Pipeline.execute(Pipeline.kt:77)
    at io.ktor.server.engine.DefaultEnginePipelineKt$defaultEnginePipeline$1$invokeSuspend$$inlined$execute$1.invokeSuspend(Pipeline.kt:478)
    at io.ktor.server.engine.DefaultEnginePipelineKt$defaultEnginePipeline$1$invokeSuspend$$inlined$execute$1.invoke(Pipeline.kt)
    at io.ktor.server.engine.DefaultEnginePipelineKt$defaultEnginePipeline$1$invokeSuspend$$inlined$execute$1.invoke(Pipeline.kt)
    at io.ktor.util.debug.ContextUtilsKt.initContextInDebugMode(ContextUtils.kt:17)
    at io.ktor.server.engine.DefaultEnginePipelineKt$defaultEnginePipeline$1.invokeSuspend(DefaultEnginePipeline.kt:118)
    at io.ktor.server.engine.DefaultEnginePipelineKt$defaultEnginePipeline$1.invoke(DefaultEnginePipeline.kt)
    at io.ktor.server.engine.DefaultEnginePipelineKt$defaultEnginePipeline$1.invoke(DefaultEnginePipeline.kt)
    at io.ktor.util.pipeline.SuspendFunctionGun.loop(SuspendFunctionGun.kt:123)
    at io.ktor.util.pipeline.SuspendFunctionGun.proceed(SuspendFunctionGun.kt:81)
    at io.ktor.util.pipeline.SuspendFunctionGun.execute$ktor_utils(SuspendFunctionGun.kt:101)
    at io.ktor.util.pipeline.Pipeline.execute(Pipeline.kt:77)
    at io.ktor.server.netty.NettyApplicationCallHandler$handleRequest$1$invokeSuspend$$inlined$execute$1.invokeSuspend(Pipeline.kt:478)
    at io.ktor.server.netty.NettyApplicationCallHandler$handleRequest$1$invokeSuspend$$inlined$execute$1.invoke(Pipeline.kt)
    at io.ktor.server.netty.NettyApplicationCallHandler$handleRequest$1$invokeSuspend$$inlined$execute$1.invoke(Pipeline.kt)
    at io.ktor.util.debug.ContextUtilsKt.initContextInDebugMode(ContextUtils.kt:17)
    at io.ktor.server.netty.NettyApplicationCallHandler$handleRequest$1.invokeSuspend(NettyApplicationCallHandler.kt:119)
    at io.ktor.server.netty.NettyApplicationCallHandler$handleRequest$1.invoke(NettyApplicationCallHandler.kt)
    at io.ktor.server.netty.NettyApplicationCallHandler$handleRequest$1.invoke(NettyApplicationCallHandler.kt)
    at kotlinx.coroutines.intrinsics.UndispatchedKt.startCoroutineUndispatched(Undispatched.kt:55)
    at kotlinx.coroutines.CoroutineStart.invoke(CoroutineStart.kt:112)
    at kotlinx.coroutines.AbstractCoroutine.start(AbstractCoroutine.kt:126)
    at kotlinx.coroutines.BuildersKt__Builders_commonKt.launch(Builders.common.kt:56)
    at kotlinx.coroutines.BuildersKt.launch(Unknown Source)
    at io.ktor.server.netty.NettyApplicationCallHandler.handleRequest(NettyApplicationCallHandler.kt:37)
    at io.ktor.server.netty.NettyApplicationCallHandler.channelRead(NettyApplicationCallHandler.kt:29)
    at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379)
    at io.netty.channel.AbstractChannelHandlerContext.access$600(AbstractChannelHandlerContext.java:61)
    at io.netty.channel.AbstractChannelHandlerContext$7.run(AbstractChannelHandlerContext.java:370)
    at io.netty.util.concurrent.AbstractEventExecutor.safeExecute(AbstractEventExecutor.java:164)
    at io.netty.util.concurrent.SingleThreadEventExecutor.runAllTasks(SingleThreadEventExecutor.java:469)
    at io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:503)
    at io.netty.util.concurrent.SingleThreadEventExecutor$4.run(SingleThreadEventExecutor.java:986)
    at io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74)
    at io.ktor.server.netty.EventLoopGroupProxy$Companion.create$lambda-1$lambda-0(NettyApplicationEngine.kt:263)
    at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30)
    at java.base/java.lang.Thread.run(Thread.java:832)

What is the problem? I don't think that I missconfigured anything. Is it a bug?

orangethunder commented 2 years ago

Same issue here. In fact, if I write client.feature_toggles == null, IntelliJ (and my gradle build) warns me that Condition 'client.feature_toggles == null' is always 'false', yet at runtime, client.feature_toggles == null evaluates to true. That sounds like a bug to me.

orangethunder commented 2 years ago

Did some more troubleshooting. Looks like the association is only available in the transaction.

fun getClient(id: Int) = transaction(db) {
  Client.findById(id)!!.also {
    println("inside transaction:")
    println(it.feature_toggles == null)
  }
}.also {
  println("after transaction")
  println(it.feature_toggles == null)
}

The above outputs:

inside transaction:
false
after transaction
true

I can only assume this is not the desired behaviour?

heli-os commented 2 years ago

@orangethunder @daniel-reinhold

Basically, Accessing Entity (Create, Read) must be done inside Transaction.

This is because LAZY is basically mapped to relationship. If you change this to EAGER, it's probably not null outside of the transaction.

But this is not recommended. It is more effective to JOIN each query than this.

orangethunder commented 2 years ago

@heli-os, it behaves this way even if I change it to Client.findById(id)!!.load(Client::feature_toggles).

I did some more googling and it turns out it's a design limitation, see https://github.com/JetBrains/Exposed/issues/656#issuecomment-542113164. So I think this issue can be closed, but perhaps it would be better if an exception is thrown when trying to access an association outside of a transaction, rather than just returning null.

thamidu commented 2 years ago

Just a guess. Can you make sure the User table has one row and that row has a UserPermission referenced, and then try again?

joc-a commented 1 year ago

Hi @daniel-reinhold, could you please try this?