[Proposal] Entity classes

It would be nice to have something like a data class for entities. As you know, data classes generate equals() and hashCode() implementations using EVERY member in the class. While that is great for value objects, it is not ideal for entity classes.
There could be a new keyword that generated equals() and hashCode() implementations using only those members that are “primary keys”.

Something like this:

entity class Foo(val id: String, val name: String) by id

or

entity class Bar(val id: String, val parentId: String, val name: String) by (id, parentId)

The most frequent use case would be to be able to compare local and remote objects using equals() considering only the PKs. For instance if we have a collection of cached entity objects, and we download new copies from the network, we could check if a remote object is already on the cache by id. With a data class, as soon as any non-PK field is different, equals() will return false. (Ok, maybe in this example that would actually be useful to detect ANY change in the object, but what if we just don’t care about the rest of the fields?). Another use case would be to use entity objects as HashMap keys, that way even if some non-PK field changes in a remote copy, the PK fields will still produce the same hash code and we could detect that it is already in the map.

I know this could be achieved with a bit of code using the current version of the language, but it is such a common use case that adding this feature would cut a lot of boilerplate. Not only that, it would put the language ahead of Java’s record classes.

I would propose that this is not a common enough solution/use-case to warrant adding the language weight of a new class modifier.

Existing ORM frameworks have examples of how one could solve this. Alternatively, you could create an Entity parent class that takes properties as arguments. Another option is to use annotations either for marking the PKs or generating methods for comparison.

And there’s always the minus 100 points to consider. Adding a modifier isn’t a trivial thing to do to a language. This kind of change would require a decent amount of research with explanations and examples as to why the alternatives are not sufficient.

Luckily, there are so many close alternatives that would allow for testing out how it would work. For example, you could implement a compiler plugin that adds an @Entity annotation. That way you protect the language from the changes and get to experiment with the solution.

I think what you try to do is conceptually wrong. You want to say Foo(1, "john") == Foo(1, "james") is true, which obviously is not. There is only one equals/hashCode per class and you want to fill it with implementation that is not really correct, but it helps you solve a very specific problem related to this class. What if then, in another functionality, you need to compare entities using a different set of properties or using all of them (e.g. you want to verify if entity was modified in relation to the one in the DB)?

You can create a separate class that holds unique props. This is much more clean and flexible, you can even have several such classes per entity to compare them differently in various places in your code. It requires some boilerplate, but I think this isn’t that bad:

class Bar(val id: String, val parentId: String, val name: String)
data class BarPK(val id: String, val parentId: String)
val Bar.pk get() = BarPK(id, parentId)

I could, but that might interfere with whatever inheritance there already is present (or might be added to) the entity classes. Even then, adding that utility parent class in every project is boilerplate. For maximum flexibility, you want to decouple inheritance from this, and instead follow a code generation approach.

No, I’m afraid I could not. That is beyond beginner level.
But even If I could, my past experience with Java is that littering the model with annotations is not a good thing.

Well that is your very personal opinion. If it was always the case that we always need to check every single field, there would be no point in having equals nor hashCode, the compiler could generate those methods for every class.

But there are cases when we just need to check one or two fields instead of every single one. In fact this is instrumental for finding elements inside collections with a good performance, since most lists use equals in their contains or indexOf methods, and hashmaps use the hashCode in their get, put, and containsKey methods.

This is why littering a model with class modifiers is may not be a good thing. If you type it @Entity or entity, they have the same effect. A compiler plugin with an annotation is a much cheaper stepping stone than changing the compiler to add a class modifier.

To clarify, I suspect both an annotation and a modifier won’t be the best solution for this problem–I could be wrong. At least with an annotation you get to experiment with the idea, increment the version faster outside of the language, and protect the language from a costly addition of a new class modifier while you collect evidence.

I forgot to bring this up earlier but why not use comparators? After all, it better decouples the class from the responsibility to perform the comparison.

1 Like

This may be a nitpick, but that’s wrong: data classes get an automatically-generated equals() and hashCode() (and toString() and copy() and componentN()) using all properties declared in the primary constructor.

Which presumably means that all you need to do is include the primary key in the primary constructor, and then declare all the remaining properties inside the class itself…?

But even if not, for me this doesn’t come close to beating the -100 points rule. In Kotlin, all those methods are one-liners if you’re only including one or two fields, so there’s much less boilerplate than you’d need in e.g. Java.

We already use compiler plugins, rather than language extensions, for Spring (e.g. to make certain classes and methods automatically open), so that seems a fairly good fit for this type of thing too.

1 Like