javers comparing lists in object giving inconsistent results
Asked Answered
A

3

5

I am struggling to see why the following doesn't give me the same results.

If I use Javers to just compare the two lists (which have items in different orders) then I get no differences as I have specified the AS_SET List comparison to ignore the order of items in the List.

If I then wrap those Lists as a property of an object the Javers returns that the elements of the List are different because or the order of the items in the List.

Should the AS_SET apply to Lists within Objects? It is as if it is being ignore

public class App {
    public static void main(String[] args) {

        List<ListItem> list1 = ImmutableList.of(
                ListItem.builder()
                        .itemName("item1")
                        .itemValue("value")
                        .build(),
                ListItem.builder()
                        .itemName("item2")
                        .itemValue("value2")
                        .build()
        );

        List<ListItem> list2 = ImmutableList.of(
                ListItem.builder()
                        .itemName("item2")
                        .itemValue("value2")
                        .build(),
                ListItem.builder()
                        .itemName("item1")
                        .itemValue("value")
                        .build()
        );

        TopLevelClass tlc1 = TopLevelClass.builder().items(list1).build();
        TopLevelClass tlc2 = TopLevelClass.builder().items(list2).build();

        Diff diff = JaversBuilder.javers().withListCompareAlgorithm(ListCompareAlgorithm.AS_SET).build().compare(list1, list2);
        System.out.println(diff);

        Diff diffTlc = JaversBuilder.javers().withListCompareAlgorithm(ListCompareAlgorithm.AS_SET).build().compare(tlc1, tlc2);
        System.out.println(diffTlc);
    }
}

Classes below:

package wibble;

import lombok.Builder;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Setter;

@Builder
@Getter
@Setter
@EqualsAndHashCode
public class ListItem {
    private String itemName;
    private String itemValue;
}
package wibble;

import lombok.Builder;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Setter;

import java.util.List;

@Builder
@Getter
@Setter
@EqualsAndHashCode
public class TopLevelClass {
    List<ListItem> items;
}

Output when the above runs:

Diff:

Diff:
    * changes on wibble.TopLevelClass/ :
    - 'items/0.itemName' changed from 'item1' to 'item2'
    - 'items/0.itemValue' changed from 'value' to 'value2'
    - 'items/1.itemName' changed from 'item2' to 'item1'
    - 'items/1.itemValue' changed from 'value2' to 'value'
Ahmednagar answered 26/4, 2018 at 16:4 Comment(0)
A
4

There are different ways of doing it (lots of them)

Method 1 - Update Code with @Id

This similar to what @kriegaex suggested and is to add a @Id annotation to your code and update the class like below

public class ListItem {
  @Id
  private String itemName;
  private String itemValue;
}

Method 2 - Entity Registration

The Method 1 has a downside that it needs code changes to your actual model and may not be always possible or desirable. In this case you need to manually register your entity

public static void main(String[] args) {

    List<ListItem> list1 = ImmutableList.of(
            ListItem.builder()
                    .itemName("item1")
                    .itemValue("value")
                    .build(),
            ListItem.builder()
                    .itemName("item2")
                    .itemValue("value2")
                    .build()
    );

    List<ListItem> list2 = ImmutableList.of(
            ListItem.builder()
                    .itemName("item2")
                    .itemValue("value2change")
                    .build(),
            ListItem.builder()
                    .itemName("item1")
                    .itemValue("value")
                    .build(),
            ListItem.builder()
                    .itemName("item3")
                    .itemValue("value3")
                    .build()

    );

    TopLevelClass tlc1 = TopLevelClass.builder().items(list1).build();
    TopLevelClass tlc2 = TopLevelClass.builder().items(list2).build();

    Javers jvc = JaversBuilder.javers().withListCompareAlgorithm(ListCompareAlgorithm.AS_SET)
            .registerEntity(new EntityDefinition(ListItem.class, "itemName"))
            .build();
    Diff diffTlc = jvc.compare(tlc1, tlc2);
    System.out.println(diffTlc);
}

The output of above run is below

* changes on com.javerstest.ListItem/item2 :
  - 'itemValue' changed from 'value2' to 'value2change'
* changes on com.javerstest.TopLevelClass/ :
  - 'items' collection changes :
    . 'com.javerstest.ListItem@306a04bf' removed
    . 'com.javerstest.ListItem@306a04fb' added
    . 'com.javerstest.ListItem@29f62baf' added
* new object: com.javerstest.ListItem/item3

And without the .registerEntity(new EntityDefinition(ListItem.class, "itemName")), it is

* changes on com.javerstest.TopLevelClass/ :
  - 'items' collection changes :
    . 'com.javerstest.ListItem@306a04bf' removed
    . 'com.javerstest.ListItem@306a04fb' added
    . 'com.javerstest.ListItem@29f62baf' added
  - 'items/0.itemName' changed from 'item1' to 'item2'
  - 'items/0.itemValue' changed from 'value' to 'value2change'
  - 'items/1.itemName' changed from 'item2' to 'item1'
  - 'items/1.itemValue' changed from 'value2' to 'value'

Method 3 - Using @IgnoreDeclaredProperties

So after clarification on later section, another way to do it is below

@IgnoreDeclaredProperties
public class ListItem {
    private String itemName;
    private String itemValue;
}

This will let the List compare work and rest items not added. But this will not let you compare a ListItem directly.

So the recommended way is to use Method 2 only, if you don't want code changes to your models and also complete flexibility

Method 4 - Using @ShallowReference

One can add a @ShallowReference to the items and it will then do a proper Set comparison

public class TopLevelClass {

    @ShallowReference
    List<ListItem> items;
}

Currently as of 02-May-18 this doesn't work because of the bug explained later

Method 5 - Using Sets

You can use Set instead of List in your class if you want

public class TopLevelClass {
   Set<ListItem> items;
}

And the updated comparison code will be

TopLevelClass tlc1 = TopLevelClass.builder().items(new HashSet<ListItem>(list1)).build();
TopLevelClass tlc2 = TopLevelClass.builder().items(new HashSet<ListItem>(list2)).build();

Javers jvc = JaversBuilder.javers().build();

With the output as below

Diff:
* new object: com.javerstest.TopLevelClass/#items/bd3fdf9ee4c8eb797ca392a1f8eb28c6
* new object: com.javerstest.TopLevelClass/#items/ad5b96d68b6742a92d330f0d98bae8b3
* object removed: com.javerstest.TopLevelClass/#items/a1961e7fd2e23b166e4d1b2acbe67263
* changes on com.javerstest.TopLevelClass/ :
  - 'items' collection changes :
    . 'com.javerstest.TopLevelClass/#items/a1961e7fd2e23b166e4d1b2acbe67263' removed
    . 'com.javerstest.TopLevelClass/#items/bd3fdf9ee4c8eb797ca392a1f8eb28c6' added
    . 'com.javerstest.TopLevelClass/#items/ad5b96d68b6742a92d330f0d98bae8b3' added

Method 6 - Register ListItem as value

In this you can register ListItem as a value

Javers jvc = JaversBuilder.javers().withListCompareAlgorithm(ListCompareAlgorithm.AS_SET)
            .registerValue(ListItem.class)
            .build();
Diff diffTlc = jvc.compare(tlc1, tlc2);
System.out.println(diffTlc);

And the output is

* changes on com.javerstest.TopLevelClass/ :
  - 'items' collection changes :
    . 'com.javerstest.ListItem@306a04bf' removed
    . 'com.javerstest.ListItem@306a04fb' added
    . 'com.javerstest.ListItem@29f62baf' added

Feature?/ BUG? /limitation?

Now another thing that is happening here in the code is below, if you look at the output from our Method2 without .registerEntity(new EntityDefinition(ListItem.class, "itemName"))

* changes on com.javerstest.TopLevelClass/ :
  - 'items' collection changes :
    . 'com.javerstest.ListItem@306a04bf' removed
    . 'com.javerstest.ListItem@306a04fb' added
    . 'com.javerstest.ListItem@29f62baf' added

This is basically because of ListCompareAlgorithm.AS_SET, if you change it to SIMPLE, the output will change to below

  - 'items' collection changes :
    0. '...ListItem/item1' changed to '...ListItem/item2'
    1. '...ListItem/item2' changed to '...ListItem/item1'
    2. '...ListItem/item3' added

So in our original code

Javers jvc = JaversBuilder.javers().withListCompareAlgorithm(ListCompareAlgorithm.AS_SET).build();
Diff diffTlc = jvc.compare(tlc1, tlc2);

First it compares the list using set only and adds the change like below

* changes on com.javerstest.TopLevelClass/ :
  - 'items' collection changes :
    . 'com.javerstest.ListItem@306a04bf' removed
    . 'com.javerstest.ListItem@306a04fb' added
    . 'com.javerstest.ListItem@29f62baf' added

But then it goes further again and does another diff on each item of the array as well, that is why an additional diff gets added

  - 'items/0.itemName' changed from 'item1' to 'item2'
  - 'items/0.itemValue' changed from 'value' to 'value2change'
  - 'items/1.itemName' changed from 'item2' to 'item1'
  - 'items/1.itemValue' changed from 'value2' to 'value'

So it is not that the AS_SET is not being picked, it is that the diff is first done for the list as set and at individual item level also. I have raise a issue about the same to understand it better

https://github.com/javers/javers/issues/669

Acculturate answered 2/5, 2018 at 12:2 Comment(0)
C
1

Disclaimer: Actually I never heard of JaVers before, I have just stumbled upon this question and became curious. So I am a complete greenhorn here.

Looking at the diff examples in the JaVers manual, you read something like:

Configuration

JaVers needs to know that the Employee class is an Entity (...) It’s enough to annotate the name field with the @Id annotation (...)

So let's try this:

package wibble;

import lombok.*;
import org.javers.core.metamodel.annotation.Id;

@Builder
@Getter
@Setter
@EqualsAndHashCode
@ToString
public class ListItem {
  @Id
  private String itemName;
  private String itemValue;
}

The console log becomes:

Diff:

Diff:
Cornute answered 2/5, 2018 at 8:8 Comment(0)
L
1

As @kriegaex already mentioned, you can map ListItem as Entity or Value and it would solve the problem. But it would be the workaround, because ListItem looks like Value Object. Another workaround would be changing List to Set.

Looks like, for now you can't compare Value Objects in List wit AS_SET algorithm. There is an issue with generating ValueObjectIds in this case. The discussion is continued here https://github.com/javers/javers/issues/669

Loosen answered 2/5, 2018 at 16:20 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.