java-object-merger - more than just a mapper of objects

    Hello! I would like to introduce you a new java library for mapping / merging objects, which I “modestly” position as a possible alternative to dozer . If you are developing enterprise applications in java, you are not indifferent to the effectiveness of your work, and you want to write less boring code, then I invite you to read on!

    UPD Posted in the central maven repository
    net.sf.brunneng.jomjava-object-merger0.8.5.1


    UPD2 . Version 0.8.4





    What are object mappers for?

    The simple answer is to copy data automatically from one object to another. But then you may ask: why do you need this copy? One can doubt that this is needed very often. So a more detailed answer should be given.
    In the world of enterprise applications, it is customary to beat the internal structure into layers: a database access layer, a business, and a presentation / web service. As a rule, objects that map onto tables in the database live in the database access layer. Let's agree to call them DTO (from Data transfer object). For the good, they only transfer data from tables and do not contain business logic. On the presentation / web services layer, there are objects that deliver data to the client (browser / web services clients). Let's call them VO (from View object). VOs may require only part of the data that is in the DTO, or aggregate data from multiple DTOs. They can additionally engage in the localization or transformation of data in a convenient form for presentation. So passing the DTO right to the presentation is not entirely correct. Also, sometimes business objects BO (Business object) are highlighted in the business layer. They are wrappers over DTO and contain the business logic of working with them: saving, modifying, business operations. Against this background, a task arises of transferring data between objects from different layers. Say, map part of the data from DTO to VO. Or from VO to BO and then save what happened.

    If you solve the problem head on, you get something like this “dumb” code:
    …
    employeeVO.setPositionName(employee.getPositionName());
    employeeVO.setPerson(new PersonVO());
    PersionVO personVO = employeeVO.getPerson();
    PersonDTD person = employee.getPerson();
    personVO.setFirstName(person.getFirstName());
    personVO.setMiddleName(person.getMiddleName());
    personVO.setLastName(person.getLastName());
    ...
    

    Is that familiar? :) If yes, then I can please you. For this problem have already come up with a solution.

    Mappers of objects

    Of course, they were not invented by me. There are many implementations in java. You can find it, for example, here .
    In short, the task of the mapper is to copy all the properties of one object to another, and also do the same recursively for all child objects, in the process doing the necessary type conversion, if necessary.
    The mappers from the list above are all different, more or less primitive. Perhaps the most powerful dozer , I worked with it for about 2 years, and some things stopped working in it. And the sluggish pace of further development of the doser was prompted to write your “bike” (yes, I met other mappers - for our requirements they are even worse).

    What is bad dozer

    1. Poor configuration support through annotations (only available @Mapping).
    2. It is impossible to map from several fields to one (for example, to collect the full name from the name, surname and patronymic).
    3. Problems with mapping generic properties. If the parent abstract class has a getter that returns a generic type of T, where , а в чайлде T определен, то при маппинге чайлда будет учитываться только спецификация типа T. Будто бы он IEntity, а не тот, которым он определен в чайлдовом классе..
      Property classes are stored as lines in the internal cache of the doser, and to get the class, a special loader class is used. Problems with this arise in the osgi environment when the dozer is in one bundle and the desired bean class in another is not accessible from the first. We overcame the problem, albeit in a standard way - by slipping the desired loader class, the implementation itself: storing the class as a string looks strange. Perhaps this is in order not to create perm gen space memorials. But still not very clear.
      If something suddenly doesn’t map, it’s very difficult to figure it out. If you deboot the doser, you will understand why. There's some kind of ... just a crazy pile of OOP patterns - everything is confusing and not explicit. However, this is only for my taste.

      What qualities should a mapper have?

      1. Extensive configuration support through annotations.
      2. Full generic support.
      3. A clean, understandable code that anyone can risk without breaking the brain.
      4. By default, without any additional settings, it should map in the way that the developer will most likely expect.
      5. It should be possible to fine-tune (no worse than the doser).


      Why merger and not mapper?

      java-object-merger differs from other mappers by one feature. The underlying idea was to give the ability to create images of objects ( the Snapshot ) at some point in time, and then comparing them to find the differences ( the Diff) is similar to how we find diff between two texts. Moreover, it should be possible to view snapshots and diffs in a human-readable textual form. So that once you look at the diff immediately all the differences become clear, as well as how the target will be changed after applying the diff. Thus, we achieve complete transparency of the process. No magic and black boxes! Creating snapshots opens up another interesting scenario. You can make a snapshot of the object, then somehow change it, make a new snapshot - check what has changed, and, if desired, roll back the changes. By the way, diff can be circumvented by a special visitor, and only mark those changes that you want to apply, and ignore the rest.
      So we can say that merger is more than just a mapper.

      Using

      The “Hello world” program looks something like this:

      import net.sf.brunneng.jom.IMergingContext;
      import net.sf.brunneng.jom.MergingContext;
      public class Main {
         public static class A1 {
            private String field1;
            public String getField1() {
               return field1;
            }
            public void setField1(String field1) {
               this.field1 = field1;
            }
         }
         public static class A2 {
            private String field1;
            public A2(String field1) {
               this.field1 = field1;
            }
            public String getField1() {
               return field1;
            }
         }
         public static void main(String[] args) {
            IMergingContext context = new MergingContext();
            A2 a2 = new A2("Hello world!");
            A1 a1 = context.map(a2, A1.class);
            System.out.println(a1.getField1());
         }
      }
      


      First, we see that for mapping it is necessary that the property has a getter on both objects. This is for comparing values. And the setter at the target to write a new value. Properties themselves must be named the same.

      Let's see how the map method is implemented. This will help to understand many things about the library.

      @Override
         public  T map(Object source, Class destinationClass) {
            Snapshot sourceSnapshot = createSnapshot(source);
            Snapshot destSnapshot = null;
            if (sourceSnapshot.getRoot().getType().equals(DataNodeType.BEAN)) {
               Object identifier = ((BeanDataNode)sourceSnapshot.getRoot()).getIdentifier();
               if (identifier != null) {
                  destSnapshot = createSnapshot(destinationClass, identifier);
               }
            }
            if (destSnapshot == null) {
               destSnapshot = createSnapshot(destinationClass);
            }
            Diff diff = destSnapshot.findDiffFrom(sourceSnapshot);
            diff.apply();
            return (T)destSnapshot.getRoot().getObject();
         }
      


      If the source snapshot is a bin, and if it has an identifier, then try to find the target bin for the destinationClass using IBeanFinder [here createSnapshot(destinationClass, identifier);]. We did not register such ones, and identifier didn’t, so we go further. Otherwise, the bean is created using the appropriate IObjectCreator [here createSnapshot(destinationClass)]. We did not register such ones either, however, in the standard delivery there is a creator of objects by the default constructor - it is used. Next, the diff from the snapshot of the source is taken from the target snapshot and applied to the target. All.

      By the way, diff, for this simple case, will look like this:
      MODIFY {
         dest object : Main$A1@28a38b58
         src object  : Main$A2@76f8d6a6
         ADD {
            dest property : String field1 = null
            src property  : String field1 = "Hello world!"
         }
      }
      


      Key annotations

      Are in the package net.sf.brunneng.jom.annotations.
      • @Mapping- sets the path to the mapping field at the other end of the association (for example “employee.person.firstName”). May be indicated on the class of the target or source object.
      • @Skip - the field does not fall into a snapshot, does not compare and does not map.
      • @Identifier- marks the field which is considered the identifier of the bin. Thus, when comparing collections, we will know which object should be compared with which. Namely, objects with matching identifiers will be compared. Also, if in the process of applying diff there is a need to create a bin, and at the same time an identifier is known, then there will be an attempt to first find this bin using registered IBeanFinders. So, an implementation IBeanFIndercan search for beans, for example, in a database.
      • @MapFromMany - the same as @Mapping is only indicated on the class of the target object and allows you to specify an array of properties on the source object that will be mapped onto the field in the target object.
      • @Converter- allows you to set the heir on the class property PropertyConverter. - he will perform the conversion between properties. The property converter is required when mapping several fields to one, because he will just have to collect all the values ​​from the source together and form one value from them.
      • @OnPropertyChange, @OnBeanMappingStarted, @OnBeanMappingFinished - allow you to mark methods that listen for the corresponding events in the life cycle of mapping that occur in this bin.
      • Other.


      Type conversions

      In IMergingContext, you can register custom type converters from one type to another (interface TypeConverter). The standard set of converters includes conversions:
      • primitive types in wrappers and vice versa
      • date conversion
      • objects in a row
      • enums to enums, and strings to enums by the name of the enum constant


      Categories of objects

      Mapper divides all objects into categories such as:
      1. Value objects: primitive types, objects in a package java.lang, dates, arrays of value objects. The list of classes considered to be values ​​can be expanded through IMergingConext.
      2. Collections are arrays, all inherited from java.util.Collection.
      3. Maps are all inherited from java.util.Map.
      4. Beans are all the rest.


      Performance

      Honestly, while I was writing the library, I didn’t think much about performance. Yes, and initially there was no goal of high performance. However, I decided to measure the mapping time N times per test object. The source code of the test . The object is quite complex, with value fields, child bins, collections, and maps. For comparison, I took dozer the latest version 5.4.0 at the moment. He expected that the doser would not leave any chances. But it turned out quite the opposite! dozer mapped 5000 test objects in 32 seconds, and java-object-merger 50,000 objects in 8 seconds. The difference is some wild - 40 times ...

      Application

      java-object-merger has been tested on the current project since my main work (osgi, spring, hibernate, hundreds of mapping classes). To replace it, the doser completely took less than 1 day. There were some obvious schools along the way, but after fixing, all the main scenarios worked fine.

      Lazy Snapshots

      One of the obvious problems found while screwing the mapper to the real project was that if you do a snapshot on a DTO that has lazy lists of other entities, and those others refer to the third ones, etc., then you can inadvertently create one snapshot, deflate the floor of the base. Therefore, it was decided to make all the properties in the snapshot lazy by default. This means that they will not be pulled out of objects until they are compared with the corresponding property when taking a diff. Or until we explicitly call the method on the snapshot loadLazyProperties(). And when pulling out a property, a snapshot is automatically completed - again with lazy properties that wait until they are loaded.

      Conclusion

      If interested - the project, with source codes and documentation is here . All the basic functionality of the library is covered by unit tests, so you can be sure that you will not see any silly trivial errors in it. Almost all classes and methods are documented by javadoc.
      Download, try, write your reviews :). I promise to respond promptly and listen to your wishes.

    Also popular now: