Support Scala case classes in the object mapper

Description

The object mapper currently does not support Scala's case classes out of the box.

For Scala, the generated bytecode does not contain getters that comply with the JavaBeans specs, and because of that, they cannot be detected.

The above class does not compile:

The output of javap show that the getters are named after the property names:

Annotating all the fields with @CqlName doesn't help.

I suggest that we relax the requirements for an entity class; instead of requiring that "there must be a getter method that follows the usual naming convention":

  1. Require either a getter or a field;

  2. Also accept getters that have the exact same name as the property.

We also have to address the need for a no-arg constructor, which data classes usually don't provide out of the box. This should be addressed by .

Environment

None

Pull Requests

None

Activity

Show:
Alexandre Dutra
June 17, 2020, 11:10 AM

FYI we should consider this as a regression since driver 3.x was able to support Scala case classes (granted, with some quirks).

See this blog post for details. Here is the working example for driver 3.x:

Olivier Michallat
June 25, 2020, 10:49 PM
Edited

What's not working with Kotlin? I did test with it, and there's a dedicated section in the manual.

Alexandre Dutra
June 26, 2020, 8:49 AM

What's not working with Kotlin? I did test with it, and there's a dedicated section in the manual.

I didn’t even know that we had such a section. I think the whole section about integration with other languages would deserve more visibility, as a separate page. Judging from the example there, the only thing missing is a non-default constructor, but that’s JAVA-2125. I will change the title of this ticket and re-focus on Scala only.

Olivier Michallat
July 31, 2020, 12:17 AM

Quick reminder of the current rules in mapper 3.8.0: must have a getter (getFoo()/isFoo(), no parameters, not void), must have a setter (single parameter, same type, return type does not matter), may have a field.

There are a number of problems that will need to be solved for Scala:

  • as you mentioned, the getters follow a different convention:

    I think the two conventions should be mutually exclusive. Otherwise we'll ineluctably run into a Scala case class where one of the field names starts with is or get, and do the wrong thing.

  • if we drop the "getFoo" requirement, the Scala code has a number of generated methods that will qualify as false positives:

    hashCode and toString should definitely be hard-coded exclusions. The product* ones could be part of a default transient properties list, that would be used when @TransientProperty is not explicitly defined. The copy$default$* ones are more problematic, because currently we don't have the ability to exclude a pattern. A quick and dirty solution would be to hard-code them in the entity factory, no one is going to name a field like that.

  • the immutable aspect should be pretty easy. Once we know a particular type is immutable (more on this below), we set a flag on the EntityDefinition, and DaoGetEntityMethodGenerator generates a different implementation that assumes a constructor with all fields.

The next question is how we detect when we need to switch to "Scala mode". We could try to auto-detect it, but it might get messy, there are probably a bunch of annoying corner cases (e.g. "getxxx" / "isyyy" field names as mentioned previously). I think it would be reasonable to require processor arguments to explicitly enable those features. Also it would allow people to use them independently, and not necessarily in the context of Scala:

With this approach the processor knows what to expect ahead of time, things get much simpler.

We already have a few processor arguments, see MapperProcessor.

Olivier Michallat
July 31, 2020, 6:17 PM

In fact there's a better option than processor arguments, we can introduce a new entity-level annotation:

It's not more complicated to handle in DefaultEntityFactory. It's a bit more user-friendly. And it also allows you to mix different approaches in the same application.

It's distinct from @Entity so that it can be inherited from an interface.

Fixed

Assignee

Olivier Michallat

Reporter

Alexandre Dutra

Labels

PM Priority

None

Affects versions

None

Fix versions

Pull Request

None

Doc Impact

None

Size

None

External issue ID

None

External issue ID

None

Epic Link

Sprint

Java 4.x

Priority

Major
Configure