Why is a *`@KomapperProjection` a `@KomapperEntity...
# komapper
d
Why is a
@KomapperProjection
a
@KomapperEntity
? It's a bit confusing, since it's not really in the database... and sometimes might not have any relationship to something in the database... (is there a Meta entry generated for it? What would be it's use?). Also it seems like it needs a
@KomapperId
even though a projection might not always have one... And it doesn't seem to take into consideration nullable fields with default values... (you have to provide it, and it can't be
null
...)
Also, can a projection be used to map the result from many joined tables?
t
Please consider @KomapperProjection as just a marker that generates extension functions like
selectAsAddress
.
is there a Meta entry generated for it?
Yes. The generated metamodel is used to instantiate a data class in a type-safe manner.
And it doesn’t seem to take into consideration nullable fields with default values.
That is intentional. Komapper has not considered default values until now.
d
Shouldnt a projection be omitted from the Meta object?
And why should there be a need for id?
t
We are considering allowing classes that do not have the
@KomapperEntity
annotation, as in the following example. In this case, there is no need to annotate
@KomapperId
.
Copy code
@KomapperProjection
data class PersonDto(val name: String, val age: Int)
I have created a pull request. Please let me know if you have any comments. https://github.com/komapper/komapper/pull/1179
d
I commented on the issue over there, thanks!
👍 1
I explained my comment in the issue (I hope it's clearer now...).
Basically, having
null
in passed in to the
selectAsXXX
functions would allow to skip passing those properties the the projection's constructor (thereby using the constructor's default value -- whether it may be
null
or some other value).
That doesn't require KSP to know or evaluate what the default values will end up being... it just involves KSP knowing that the property is nullable or that it has a default value (at worst, if KSP doesn't know it has a default value, maybe a special annotation could be used to tell it, or those properties could be listed in
@KomapperProjection(propertiesWithDefaults=["prop1"])
or something like that...
t
Thank you for your opinion. To use default values when calling the constructor of a Data class, it is necessary to statically determine the arguments passed to the constructor. Therefore, I believe it is impossible to implement the method you propose.
at worst, if KSP doesn’t know it has a default value, maybe a special annotation could be used to tell it, or those properties could be listed in @KomapperProjection(propertiesWithDefaults=[“prop1”]) or something like that...
I think using annotations is limited in expressiveness because annotations only support specific types.
d
Then maybe having the annotation on the property itself?
Also, I don't know if this is related but Kotlin Inject somehow uses default values in KSP... https://github.com/evant/kotlin-inject?tab=readme-ov-file#default-arguments
Another little thing is if nullable properties are handled in projections (not related to default values):
Copy code
@KomapperProjection
data class Proj(val foo: String?)

query.selectAsProj(foo = // how would I set this to null if needed?
)
If default values are considered, I think the difference would be
ColumnExpression<String, String>?
where null represents "use a default value", whereas
ColumnExpression<String?, String?>
set the field's value to
null
in the db (although there seems to be no such thing as
literal(null)
...).
It seems like there's a way to know if there's a default argument there in KSP...
t
Copy code
query.selectAsProj(foo = // how would I set this to null if needed?
)
I plan to add a null expression to represent NULL in SQL.
Copy code
query.selectAsProj(foo = nullLiteral())
👍🏼 1
It seems like there’s a way to know if there’s a default argument there in KSP...
KSP knows whether there is a default value, but it cannot retrieve the default value.
d
Over there they seem to use that information to instantiate the class with the default arguments... wouldn't it be the same here?
t
I believe kotlin-inject and Komapper are trying to solve different problems. In kotlin-inject, I imagine that it determines whether to use default values for each class. On the other hand, the feature you are asking for in Komapper requires deciding whether to use default values for each instance.
d
Something like this could be done:
Copy code
@KomapperProjection
data class Proj(val id: Int, val foo: String?)
...
    override fun newEntity(m: Map<org.komapper.core.dsl.metamodel.PropertyMetamodel<*, *, *>, Any?>) = Proj(
        // By default, only instantiate with required arguments.
        `id` = m[this.`id`] as Int,
).let { proj ->
  // not sure about this check, but whatever it takes to check if it has a default argument...
  m[this.`name`]?.let { proj.copy(name = it) }
}
But, yeah, I agree it would be a bit messy 🤔, and then if there's a bunch of them... 🤕
I guess the hard part is generating the instantiation of the class according to the parameters... if there would have been a way to carry over the defaults to the
selectAsXXX
function, I guess that would have been ideal... for simple defaults, that could be done with some kind of annotation on the property to specify the type and value... but that's a bit more limiting than actually using the default values themselves...
t
I don’t think resolving it with annotations is the best approach. How about using the SQL coalesce function?
Copy code
selectAsProj(foo = coalesce(p.foo, literal("default")))
See also https://www.komapper.org/docs/reference/query/querydsl/expression/#conditional-expression-coalesce
d
That's only if you have the
p.foo
to put there in the first place...
Sometimes projections just don't always have all the fields in the query to fill them up. When it's just one or two, it's not so bad, but when it's 10-15, or nested, then...
Also can this feature handle nested classes?
Copy code
@KomapperProjection
data class Foo(val bar: Bar)

@KomapperProjection
data class Bar(val id: Int, val baz: String)

query.selectAsFoo(bar = Bar(p.id, p.bar))
Maybe this is too much, but this would be great for mapping straight to api responses...
The thing that would get even more complex would be in one-to-many queries... then a projection could have a property
val addresses: List<Address>
...
It would avoid the complications of mapping with
EntityStore
...
t
The following code is supported.
Copy code
@KomapperProjection
data class Foo(@KomapperEmbedded val bar: Bar)

data class Bar(val id: Int, val baz: String)

query.selectAsFoo(`bar#id` = p.id, `bar#baz` = p.bar))
d
Nice 😉! Although the names are a bit funny... but that's fine. What about one-to-many, etc... any way that could be supported w/o too much trouble?
t
We do not plan to support relationships for objects retrieved using @KomapperProjection. Since those objects are not entities, they do not possess uniqueness, which makes it impossible to construct relationships.
🤔 1