Platinum Partner
java,tips and tricks,tools & methods,ujorm,programming style,key-value

Key-Value Coding in Java

logoThe guide to the ujo-core module of the Ujorm framework

Introduction

Ujorm is a library for developing Java applications built on a key-value architecture of domain objects that access their attributes using objects called keys only. A Key consists of an immutable static object that is part of a domain class and provides several services. A Key in Ujorm never contains business data - unlike a similar framework, Joda Beans by author Stephen Colebourne, where attributes are contained in the Property object type.

The architercure of Ujo objects was described in the original documentation, this article demonstrates some new or interesting uses of Ujo on short examples that are based on the ujo-core module and therefore are not related with database queries. The core module can be loaded into your project using the Maven dependency:

        <dependency>
            <groupId>org.ujorm</groupId>
            <artifactId>ujo-core</artifactId>
            <version>1.45</version>
        </dependency>

Code samples are based on two entities, Employee and Company, of the Ujo type, while each of them contains (generated) setters and getters so that the object meets the JavaBeans specification.

key-value-model

Writing and reading values

We can skip the description of reading and writing attributes to JavaBeans here and go straight to reading and writing values ​​using the API of the Ujorm framework.

        Employee person = new Employee();

        // Write:
        person.set(ID, 7L);
        person.set(NAME, "Pavel");
        person.set(WAGE, 20.00);
        person.set(COMPANY, new Company());

        // Read:
        Long id = person.get(ID);
        String name = person.get(NAME);
        Double wage = person.get(WAGE);
        Company company = person.get(COMPANY);

        assert id == 7L;
        assert name == "Pavel";
        assert wage == 20.00;
        assert company != null;

To shorten the code in the above examples, the necessary static keys of the used Ujo objects were imported in the head of the class by writing:

        import static org.Company.CITY;
        import static org.Employee.*;

The above example explains how to use the key to access the related object attributes, however it does not now seem that the keys would have any benefits in comparison with POJO. I will try to convince you about the benefits of this solution in the following sections.

Restoring default values

Each Key contains its default value, which can be obtained using the Key.getDefault() method. What is more interesting is reading an attribute with a NULL value, because the Key replaces NULL attributes with its default value automatically. If the default value is NULL too, the replacement is insignificant. To restore all default values ​​it is therefore sufficient to assign each attribute with a NULL value this way:

        Employee employee = getEmployee();

        for (Key key : employee.readKeys()) {
            employee.set(key, null);
        }

        assert employee.getWage() == 0.0
               : "Default value is zero";
        assert employee.getWage() == WAGE.getDefault()
               : "Check the default value";

Another version of the previous example restores the default values ​​for numeric attributes only:

        Employee employee = getEmployee();

        for (Key key : employee.readKeys()) {
            if (key.isTypeOf(Number.class)) {
                employee.set(key, null);
            }
        }

The Ujo.readKeys() method returns all the direct keys of the current class, including parent classes, so restoring default values ​will affect all parent attributes, if there are any. For comparison of the type of the key value, the appropriate method is Key.isTypeOf(Class).

Shallow copy of an object

Because we know how to get a list of all direct keys from the Ujo object (description of composite keys will follow), we can modify the body cycle easily to create a shallow copy of the source object:

        Employee source = getEmployee();
        Employee target = source.getClass().newInstance();

        for (Key<Ujo,?> key : source.readKeys()) {
            key.copy(source, target);
        }

        assert source.getId() == target.getId()

Validation of attributes when writing

Validators are immutable objects that can be combined using AND / OR operators and which can be inserted into the key optionally when it is being built. If the key gets a validator once, it will keep checking every value at the time of entry into the Ujo object forever.

        Key<Employee, String> NAME = keyFactory.newKey(length(7));

The NAME key has now got a validator to check any employee name is up to 7 characters long. The next example tests the validator on a name that meets the limit and a name that is too long.

        final String correctName = "1234567";
        final String wrongName = "12345678";

        Employee employee = new Employee();
        employee.set(NAME, correctName);

        try {
            employee.set(NAME, wrongName);
        } catch (ValidationException e) {
            String expected
                = "Text length for Employee.name must be between 0 and 7, "
                + "but the input length is: 8";
            assert expected.equals(e.getMessage());
        }

        assert employee.getName() == correctName;

The API of validators offers support for localised error messages using templates.

        final ValidationException exception = getException();

        String template = "The name can be up to ${MAX} characters long, not ${LENGTH}.";
        String expected = "The name can be up to 7 characters long, not 8.";
        String result = exception.getError().getMessage(template);

        assert expected.equals(result);

Each object of the ValidationError type has one default template for the logging. Other examples of the use of validators are available from here.

I am considering using annotations from JSR 303 over the key definition as an alternative to direct insertion of keys using Java code - in one of the next versions of Ujorm.

Composite Keys

Two logically related keys can be joined using the Key.and(key) method, where the result is an instance of a composite key of the CompositeKey type which is also a type of the original interface Key. Because of this, composite keys can be used the same way as direct keys - including reading and writing values ​​of domain objects or joining with other composite keys. Working with composite keys also brings us some new interesting features when reading and writing values:

  • If you are reading any attribute of an object with missing relations, the result is always NULL, therefore there is no NullPointerException, which is typical for chaining of JavaBeans getters. This rule does not apply to the default object.
  • If you are writting a new value to an attribute of undefined relation, the composite Key automatically creates the missing related objects in all cases. If we are dissatisfied with such behavior for some reason, we can use the CompositeKey.setValue(Ujo, value, createRelations) method with the last parameter being false.

Example of use:

        Key<Employee,String> companyNameKey = Employee.COMPANY.add(Company.NAME);

        Employee employee = new Employee();
        String companyName = employee.get(companyNameKey); // !1
        assert companyName == null;

        employee.set(companyNameKey, "Prague"); // !2
        companyName = employee.get(companyNameKey);

        assert employee.getCompany() != null;
        assert companyName == "Prague";

The first line builds a composite key. The line marked with the first exclamation mark returns the name of the company of the employee as NULL, even though the company has not been created yet. The line marked with the second exclamation mark first adds the missing instance of the company and then writes its name into it.

If you are interested in composite keys behaviour, you can study other examples in this TestCase.

Criterion as a condition model

Using keys, values, and operators it is possible to describe the logical condition of a Ujo object for which the Criterion class is to be used. Objects of the Criterion type are immutable serializable instances that can be combined into a binary tree using AND / OR. A Criterion object is also used in the ORM module as a template to build WHERE phrases in SQL commands, however this object is completely autonomous and can be used even outside the ORM - for example to validate objects.

        Criterion<Employee> validator = Employee.WAGE.whereGt(100.0);

        try {
            validator.validate(getEmployee()
            , "Minimal WAGE is: %f."
            , validator.getRightNode());
        } catch (IllegalArgumentException e) {
            assert e.getMessage() != null;
        }

In this example we use Criterion to check that the employee has a salary higher than 100 (units).

The significance of the Criterion remains in its ability to convey a description of conditions to other modules or systems, which can then provide a custom implementation solution.

Criterion for filtering collections

The following example is a different version of the previous example and is used for filtering a collection of employees whose company is based in Prague. The example shows that composite keys can enter into Criterion:

        List<Employee> employees = COMPANY.add(CITY)
            .whereEq("Prague")
            .evaluate(getEmployees());

        for (Employee employee : employees) {
            System.out.println(employee);
        }

        assert employees.size() == 4;

It is useful to know that the value of the Criterion can also be a key:

        List<Employee> employees = COMPANY.add(CITY)
            .whereEq(Employee.NAME)
            .evaluate(getEmployees());

        assert employee.size() == 1; 

The second example filters all employees whose name matches the name of the town (seat) of their company. By the way, I agree with everyone who thinks this is not really a typical example :).

Collection Sorting

Ujorm provides a useful class for collection sorting called UjoComparator. It is not necessary to implement a new class for a large group of requirements (unlike JavaBeans collections, although Java 8 has brought a simplification), just supply the factory method with the required list of keys along with information about the direction of sort, see Key.descending(). The method always creates a new instance of Key, flagged descending sort.

        List<Employee> employees = UjoComparator.of
            ( COMPANY.add(CITY)
            , NAME.descending())
            .sort(getEmployees());

        for (Employee employee : employees) {
            System.out.println(employee);
        }

The descending() method actually creates a new composite key with one member, which provides information about the descending sort. Modeling the downward shift is also used in the ORM module.

Serialization of keys

Each direct key in ClassLoader has its own unique instance, like an item of the enumerated type Enum. In order to serialize the keys, you must insert them into a keyring represented by the class KeyRing. These key chains are not entirely new to us because the Ujo.readKeys() method used above returns (in the default implementation) this type.

        final KeyRing<Employee> keyRing1, keyRing2;
        keyRing1 = KeyRing.of(Employee.ID, Employee.COMPANY.add(Company.NAME));
        keyRing2 = service.serialize(keyRing1);

        assert keyRing1 != keyRing2 : "Different instances";
        assert keyRing1.get(0) == keyRing2.get(0) : "The same direct keys";
        assert keyRing1.get(1).equals(keyRing2.get(1)) : "The equal composite keys";
        assert new Employee().readKeys() instanceof KeyRing : "readKeys() returns the KeyRing";

Import from CSV format

The last example describes how to import text from CSV (Comma Separated Value), which offers the possibility to import attributes using composite Keys too, so you can import the attributes of relational entities. The description of CSV columns is defined by a list of keys of the (imported) Ujo object. Contents of a text CSV file with a header are the following:

        id;name;companyId
        1;Pavel;10
        2;Petr;30
        3;Kamil;50

Java code to import CSV file:

        Scanner scanner = new Scanner(getClass().getResourceAsStream("employee.csv"), "utf-8");

        UjoManagerCSV<Employee> manager = UjoManagerCSV.of
            ( Employee.ID
            , Employee.NAME
            , Employee.COMPANY.add(Company.ID));

        List<Employee> employes = manager.loadCSV(scanner, this);

        assert employes.size() == 3;
        assert employes.get(0).getId().equals(1L);
        assert employes.get(0).getName().equals("Pavel");
        assert employes.get(0).getCompany().getId().equals(10L);

If we use a class of type Ujo instead of a list of keys, the CSV file contents are imported by all direct Keys of the class. The order of the static Ujo Keys is guaranteed in the Ujorm framework and is same as the order of the static field specified in the domain class - as opposed to an undefined order field of JavaBeans.

Conclusion

The ujo-core module of the Ujorm framework provides various other tools for general use, another issue is the use of Ujo objects in ORM. There is a motivating application Demo Hotels for more examples including an ORM code to more study. Maybe some parts of the application need special descriptions, perhaps this will be done in a separate article some other time. All model examples are included in the Ujorm project and the main class is available from here.

{{ tag }}, {{tag}},

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}
{{ parent.authors[0].realName || parent.author}}

{{ parent.authors[0].tagline || parent.tagline }}

{{ parent.views }} ViewsClicks
Tweet

{{parent.nComments}}