Search code examples
javakotlinjavers

JaVers detects changes in childs if simple property in root object is changed


I use Kotlin and I am trying to compare two complex objects (with multiple cycles) with JaVers. These objects use multiple Id-Properties. Therefore I created Id-Classes to have a single Id-Property for every class. In the Id-Classes I also use references to the root objects because I need to use them to create the primary key for my database.

When I compare two objects with a single change in the root object, JaVers should only list one ValueChange. But instead JaVers finds 5 changes instead (NewObject-child, ObjectRemoved-child,ReferenceChanged-child,ListChange-root,ValueChanged-root). Trying to solve this issue, I updated my equals and hashCode methods for the child objects to check the id of the root object instead of the root object itself when calculating equality ==> root1.childList == root2.childList returns true. Any ideas how I can teach JaVers that no child object has changed?

League.kt - root object

@Entity
data class League(@EmbeddedId val leagueId: LeagueId? = null,
                  var name: String? = null,
                  var region: String? = null,
                  @OneToMany(cascade = [CascadeType.ALL], orphanRemoval = true)
                  var groups: List<TeamGroup>? = null,
                  @OneToMany(cascade = [CascadeType.ALL], orphanRemoval = true)
                  var matchDays: List<MatchDay>? = null) : Serializable {

    override fun equals(other: Any?): Boolean {
        if (this === other) return true
        if (javaClass != other?.javaClass) return false

        other as League

        if (leagueId != other.leagueId) return false
        if (name != other.name) return false
        if (region != other.region) return false
        if (groups?.map { it.teamGroupId }?.toSet() != other.groups?.map { it.teamGroupId }?.toSet()) return false
        if (matchDays?.map { it.matchDayId }?.toSet() != other.matchDays?.map { it.matchDayId }?.toSet()) return false

        return true
    }

    override fun hashCode(): Int {
        var result = leagueId?.hashCode() ?: 0
        result = 31 * result + (name?.hashCode() ?: 0)
        result = 31 * result + (region?.hashCode() ?: 0)
        result = 31 * result + (groups?.map { it.teamGroupId }?.toSet()?.hashCode() ?: 0)
        result = 31 * result + (matchDays?.map { it.matchDayId }?.toSet()?.hashCode() ?: 0)
        return result
    }
}

LeagueId.kt - root object Id

data class LeagueId(val season : String? = null, val abb : String? = null) : Serializable {

    override fun equals(other: Any?): Boolean {
        if (this === other) return true
        if (javaClass != other?.javaClass) return false

        other as LeagueId

        if (season != other.season) return false
        if (abb != other.abb) return false

        return true
    }

    override fun hashCode(): Int {
        var result = season?.hashCode() ?: 0
        result = 31 * result + (abb?.hashCode() ?: 0)
        return result
    }
}

TeamGroup.kt - child object

@Entity
data class TeamGroup(@EmbeddedId val teamGroupId: TeamGroupId? = null,
                     val name: String? = null,
                     val mode: String? = null,
                     val tableMode: Int? = null,
                     @OneToMany(mappedBy = "group", cascade = [CascadeType.ALL], orphanRemoval = true)
                     var teams: List<Team>? = null,
                     @OneToMany(mappedBy = "group", cascade = [CascadeType.ALL], orphanRemoval = true)
                     var matches: List<Match>? = null,
                     var remarks: String? = null,
                     @OneToMany(cascade = [CascadeType.ALL], orphanRemoval = true)
                     var rows: List<Row>? = null) : Serializable {

    override fun toString(): String {
        return "TeamGroup(id=${teamGroupId?.id}, nr=${teamGroupId?.nr}, name=$name, mode=$mode, " +
                "tableMode=$tableMode, teams=$teams, matches=$matches, remarks=$remarks, rows=$rows, " +
                "league=${teamGroupId?.league?.leagueId?.season}-${teamGroupId?.league?.leagueId?.abb})"
    }

    override fun equals(other: Any?): Boolean {
        if (this === other) return true
        if (javaClass != other?.javaClass) return false

        other as TeamGroup

        if (teamGroupId != other.teamGroupId) return false
        if (name != other.name) return false
        if (mode != other.mode) return false
        if (tableMode != other.tableMode) return false
        if (teams?.map{it.id}?.toSet() != other.teams?.map{it.id}?.toSet()) return false
        if (matches?.map{it.matchId}?.toSet() != other.matches?.map{it.matchId}?.toSet()) return false
        if (remarks != other.remarks) return false
        if (rows?.map{it.rowId}?.toSet() != other.rows?.map{it.rowId}?.toSet()) return false

        return true
    }

    override fun hashCode(): Int {
        var result = teamGroupId?.hashCode() ?: 0
        result = 31 * result + (name?.hashCode() ?: 0)
        result = 31 * result + (mode?.hashCode() ?: 0)
        result = 31 * result + (tableMode ?: 0)
        result = 31 * result + (teams?.map{it.id}?.toSet()?.hashCode() ?: 0)
        result = 31 * result + (matches?.map{it.matchId}?.toSet()?.hashCode() ?: 0)
        result = 31 * result + (remarks?.hashCode() ?: 0)
        result = 31 * result + (rows?.map{it.rowId}?.toSet()?.hashCode() ?: 0)
        return result
    }
}

TeamGroupId.kt - child object id

data class TeamGroupId(@ManyToOne val league: League? = null, val id : Int? = null, val nr : Int? = null) : Serializable {

    override fun equals(other: Any?): Boolean {
        if (this === other) return true
        if (javaClass != other?.javaClass) return false

        other as TeamGroupId

        if (league?.leagueId != other.league?.leagueId) return false
        if (id != other.id) return false
        if (nr != other.nr) return false

        return true
    }

    override fun hashCode(): Int {
        var result = league?.leagueId?.hashCode() ?: 0
        result = 31 * result + (id ?: 0)
        result = 31 * result + (nr ?: 0)
        return result
    }
}

Update

The problem is the reference to the root object within the id of the child object. If I remove this reference from the id and move to the object itself only one change is detected by JaVers. Due to my data model I am not sure if I can remove this references in every id object. @DiffIgnore is not working within the Id-Property because it is handled as a ValueObject.


Solution

  • The problem is caused by wrong InstanceId values of your Entities. Since you have complex objects as Entity IDs, JaVers uses reflectiveToString() function to create String representations of IDs. In your case, it produces really bad results, because you have cycles (ID has a reference to owning Entity).

    Fortunately, you can register a custom toString() function using JaversBuider.registerValueWithCustomToString() , for example:

    @TypeName("Entity")
    class Entity {
        @Id Point id
        String data
    }
    
    class Point {
        double x
        double y
    
        String myToString() {
            "("+ (int)x +"," +(int)y + ")"
        }
    }
    
    def "should use custom toString function for complex Id"(){
      given:
      Entity entity = new Entity(id: new Point(x: 1/3, y: 4/3))
    
      when: "default reflectiveToString function"
      def javers = JaversBuilder.javers()
              .build()
      GlobalId id = javers.getTypeMapping(Entity).createIdFromInstance(entity)
    
      then:
      id.value() == "Entity/0.3333333333,1.3333333333"
    
      when: "custom toString function"
      javers = JaversBuilder.javers()
              .registerValueWithCustomToString(Point, {it.myToString()})
              .build()
      id = javers.getTypeMapping(Entity).createIdFromInstance(entity)
    
      then:
      id.value() == "Entity/(0,1)"
    }
    

    See also updated doc about Entity ID https://javers.org/documentation/domain-configuration/#entity-id