Accessing Domain Objects without Domain Classes - BETA

Hazelcast offers a generic object interface (GenericRecord) that is returned to the user when the domain class is missing on the classpath. For example, if PortableFactory is not given in the serialization configuration for a portable object, the user domain class cannot be created, and Hazelcast returns GenericRecord instead. In the previous Hazelcast IMDG releases, we were throwing HazelcastSerializationException("Could not create Portable for class-id: " + classId) for the same situation.

GenericRecord is an immutable object. It allows you to read the field of objects via the related field names. GenericRecord is applicable only to Portable objects.

You can use this feature when the cluster does not have the domain classes of the clients in a client-server architecture. On remote calls like distributed executor service or entry processors, you may need to access the domain object. In case the class of the domain object is not available on the cluster, GenericRecord allows to access, read and write the objects back without the class of the domain object on the classpath. Here is a read example with entry processor:

    map.executeOnKey(key, (EntryProcessor<Object, Object, Object>) entry -> {
             Object value = entry.getValue();
             GenericRecord genericRecord = (GenericRecord) value;

             int id = genericRecord.readInt("id");

             return null;
         });

An alternative approach introduced in the previous Hazelcast IMDG releases is the User Code Deployment feature to deploy the classes from the client to the cluster. However, it has a restriction: you can not upload a new version of your class to the cluster if you use the portable versioning support. Loading two different versions of the same class on the JVM is not a problem that we want to solve: using GenericRecord, you can easily write different versions of your classes from the clients and access them without using the User Code Deployment feature.

With the introduction of GenericRecord, User Code Deployment should be used only for functional objects like Runnable, Callable and EntryProcessor.

You can also create a GenericRecord in portable format with GenericRecord.Builder as follows:

ClassDefinition classDefinition = new ClassDefinitionBuilder(PORTABLE_FACTORY_ID, EMPLOYEE_CLASS_ID)
                        .addUTFField("name").addIntField("id").build();

GenericRecord namedRecord = GenericRecord.Builder.portable(classDefinition)
                .writeUTF("name", "foo")
                .writeInt("id", 123).build();

Note that the class definitions are better to be created once and used when creating different instances of the same type GenericRecord.

We have also added two convenience methods in GenericRecord for you to avoid passing a class definition. For example, if you want to modify a value and put it back using an entry processor, you don’t need to create a class definition. Instead you can create a builder from GenericRecord which carries the same class definition as follows:

map.executeOnKey("key", (EntryProcessor<Object, Object, Object>) entry -> {
            GenericRecord genericRecord = (GenericRecord) entry.getValue();
            GenericRecord modifiedGenericRecord = genericRecord.newBuilder()
                    .writeUTF("name","Kermit")
                    .writeLong("id", 4)
                    .writeInt("age",20)
                    .writeUTF("surname", "The Frog").build();
            entry.setValue(modifiedGenericRecord);
            return null;
        });

Another convenience method is cloneWithBuilder. This is useful if you want to update only a couple of fields from the original genericRecord. In that case, the new builder carries both classDefinition and values from the original genericRecord. Here is the same example where we just update the age:

map.executeOnKey("key", (EntryProcessor<Object, Object, Object>) entry -> {
            GenericRecord genericRecord = (GenericRecord) entry.getValue();
            GenericRecord modifiedGenericRecord = genericRecord.cloneWithBuilder()
                    .writeInt("age",22).build();
            entry.setValue(modifiedGenericRecord);
            return null;
        });

Another use case of this feature is on the client side (could also be a member): GenericRecord allows to read from/write to a cluster without having the related classes on the classpath. A client could work with the cluster without introducing the PortableFactory at the start. In this case, the client works with GenericRecords instead of domain classes. An example code snippet on the client side with a map is shown below:

        GenericRecord record = (GenericRecord) map.get("key1");
        String name = record.readUTF("name");
        int id = record.readInt("id");

        GenericRecord newGenericRecord = genericRecord.cloneWithBuilder()
                .writeInt("age",22).build();

        map.put("key2", newGenericRecord);