Skip to content
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

Added support for self references (#10) #11

Merged
merged 11 commits into from Jul 9, 2020

Conversation

Namnodorel
Copy link

@Namnodorel Namnodorel commented Jun 18, 2020

I found this project appealing enough to implement this myself :D

I added support for objects having M2M-relations to themselves by distinguishing the attributes of the join-table with "source" and "target" keywords, and changed buildToEntityMapFunc() to avoid infinite recursions in that case (and it also now passes the proper object references of related objects).

Because this PR changes how krush expects a join-table to look, this can be considered a breaking change.

Also: I did not do much testing of this change apart from running the unit tests and testing it with my use case (both work without issue Looks like I overlooked the "example" tests - will look into this), so this will likely need to be tested with more examples before it can be merged with confidence that there will be no unfortunate surprises.

@Namnodorel
Copy link
Author

Okay, I know what the problem is: With the updated implementation, krush will return objects with "perfect" references - meaning multiple objects pointing to the same entity object will all point to the same object instance. Every object from the DB exists only once in memory as well.

The problem that arises is that in bidirectional relations, that means that toString(), equals() and hashCode() will just loop infinitely between two objects that reference each other. The first solution I have thought of would be to exclude the bidir-fields from these methods, but afaik the only way to do that in Kotlin is to override the methods with manual implementations.

I don't really know what to do about this, would appreciate any input.

@Pilleo
Copy link

Pilleo commented Jun 19, 2020

Aren't only fields declared in constructor used in equals method?
https://kotlinlang.org/docs/reference/data-classes.html#properties-declared-in-the-class-body

@Namnodorel
Copy link
Author

Aren't only fields declared in constructor used in equals method?
https://kotlinlang.org/docs/reference/data-classes.html#properties-declared-in-the-class-body

If you defined all bidirectional fields outside of the constructor, krush would have to require the user to make them mutable, or implement strange secondary constructors. Certainly works, but I'm not sure whether it's an okay restriction for krush to have.

Namnodorel added 2 commits June 20, 2020 19:07
(This will break DB schemes that were generated using old versions of krush)
(and prevent infinite recursion for self-references)
@pjagielski
Copy link
Collaborator

Hi, thanks for your submission.
Yes, before your changes we prevented these problems by keeping the references "imperfect". Take a look at TreeTest: the Leafs have references to Branches with empty Leaf list. If you want to traverse whole graph of objects you have to start from the root, this will give you access to all fetched objects, without lazy fetching.
Is it possible to make this similar in you case? So the Tasks graph will rather be a tree, not a DAG?

@Namnodorel
Copy link
Author

Well, my specific use case allows for using a tree-structure instead of a graph, yes. But my requirement is not that I can traverse the graph/tree arbitrarily(in fact, I don't use anything bidir), but that the references point to the right object.

My line of thought was that it would be unfortunate if the application made a change to a on object in one place, and then later had to work with another object that still holds the reference to the (now outdated) copy. To get around such bugs, the app would have to either reload data from the DB frequently, or, for every change, execute it for both the original object and all of its copies (which sounds awful to develop and maintain). I think this problem will tend to happen in other cases too when you're working with copies instead of references.

@pjagielski
Copy link
Collaborator

Well, the design of krush is mostly focused on fetching data (read model). Because of the data classes, the fetched model is immutable. For simple CRUD operations we provide a way of updating data by passing a object copy with modifications, but for complex updates we recommend using native Exposed API. This also worked really well in our projects.

What I'm thinking about is a configuration option when the user can choose whether references or copies are used in relations. For references, the relation fields should be removed from constructor for proper equals/hashCode as @Pilleo mentioned, to avoid infinite recursion. IMO the default should be copies to avoid user confusion regarding StackOverflow errors.

@sledzmateusz @Namnodorel what do you think?

@Namnodorel
Copy link
Author

Sounds like a reasonable solution. Where would you want to put such a configuration option? My first thought would be to add an optional parameter like copyRelated: Boolean = true to the to...List() and to...Map() functions, move the implementations to their own private functions, and choosing which one to use based on the parameter.

@Namnodorel
Copy link
Author

(I'll happily implement this, but am currently waiting for confirmation that it's the desired solution)

@pjagielski
Copy link
Collaborator

Hi, sorry for delay. I was rather thinking of some global configuration option for the source code generator. For now we use only kapt.kotlin.generated which is standard kapt option, we could introduced something like kapt.krush.references which could be set to copy (default) or real. This option should be pass then to MappingsGenerator. WDYT?
Generating multiple methods could be confusing for users.

@Namnodorel
Copy link
Author

Okay, I have now added a kapt config option that lets you switch between the current and real-reference implementations. It can be used like

kapt {
    arguments {
        arg("krush.references", 'real')
    }
}

I had to comment out the test I added though, because AFAIK there is now way to set such Gradle options per-test. So to test that functionality, one has to set the Gradle option, uncomment the test and keep in mind that the BiDir-Tests won't be happy about the change.

.map { "\t${it.name} = this.to${it.target.simpleName}()"}

val mapping: String
if(generateRealReferences){
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please reformat if statements as in the rest of the code (space after "if", "else")

@@ -84,6 +95,14 @@ class MappingsGenerator : SourceGenerator {
}

private fun buildToEntityMapFunc(entityType: TypeElement, entity: EntityDefinition, graphs: EntityGraphs): FunSpec {
return if(generateRealReferences){
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe it would be better to do it by inheritance? buildToEntityMapFunc would be abstract and we would have 2 implementations of MappingsGenerator - CopiedReferencesMappingsGenerator and RealReferencesMappingsGenerator

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, I feel like creating two whole new classes for one changed method is a bit much. Well, it's your call if you prefer it this way ^^

class KrushAnnotationProcessor : AbstractProcessor() {

companion object {
const val KAPT_KOTLIN_GENERATED_OPTION_NAME = "kapt.kotlin.generated"
const val KRUSH_REFERENCES_OPTION_NAME = "krush.references"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please add (commented) the configuration you mentioned in comment into example/build.gradle

@pjagielski pjagielski merged commit ac8f99d into TouK:master Jul 9, 2020
@Namnodorel Namnodorel deleted the self-references branch July 15, 2020 12:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants