blob: bef6dc0a36935fcb125109e679ff2b3c0b945f7a [file] [log] [blame]
// Licensed to the Apache Software Foundation (ASF) under one or more
// contributor license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright ownership.
// The ASF licenses this file to You under the Apache License, Version 2.0
// (the "License"); you may not use this file except in compliance with
// the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
= Binary Marshaller
== Basic Concepts
Binary Marshaller is a component of Ignite that is responsible for data serialization. It has the advantages:
* It enables you to read an arbitrary field from an object's serialized form without full object deserialization.
This ability completely removes the requirement to have the cache key and value classes deployed on the server node's classpath.
* It enables you to add and remove fields from objects of the same type. Given that server nodes do not have model classes
definitions, this ability allows dynamic change to an object's structure, and even allows multiple clients with different versions of class definitions to co-exist.
* It enables you to construct new objects based on a type name without having class definitions at all, hence
allowing dynamic type creation.
Binary objects can be used only when the default binary marshaller is used (i.e. no other marshaller is set to the configuration explicitly).
[NOTE]
====
[discrete]
=== Restrictions
There are several restrictions that are implied by the BinaryObject format implementation:
* Internally, Ignite does not write field and type names but uses a lower-case name hash to identify a field or a type.
It means that fields or types with the same name hash are not allowed. Even though serialization will not work out-of-the-box
in the case of hash collision, Ignite provides a way to resolve this collision at the configuration level.
* For the same reason, BinaryObject format does not allow identical field names on different levels of a class hierarchy.
* If a class implements `Externalizable` interface, Ignite will use `OptimizedMarshaller` instead of the binary one.
The `OptimizedMarshaller` uses `writeExternal()` and `readExternal()` methods to serialize and deserialize objects of
this class which requires adding classes of `Externalizable` objects to the classpath of server nodes.
====
The `IgniteBinary` facade, which can be obtained from an instance of Ignite, contains all the necessary methods to work with binary objects.
[NOTE]
====
[discrete]
=== Automatic Hash Code Calculation and Equals Implementation
There are several restrictions that are implied by the BinaryObject format implementation:
If an object can be serialized into a binary form, then Ignite will calculate its hash code during serialization and
write it to the resulting binary array. Also, Ignite provides a custom implementation of the equals method for the binary
object's comparison needs. This means that you do not need to override the GetHashCode and Equals methods of your custom
keys and values in order for them to be used in Ignite, unless they can not be serialized into the binary form.
For instance, objects of `Externalizable` type cannot be serialized into the binary form and require you to implement
the `hashCode` and `equals` methods manually. See Restrictions section above for more details.
====
== Configuring Binary Objects
In the vast majority of use cases, there is no need to additionally configure binary objects.
However, in a case when you need to override the default type and field IDs calculation, or to plug in `BinarySerializer`,
a `BinaryConfiguration` object should be defined in `IgniteConfiguration`. This object allows specifying a global
name mapper, a global ID mapper, and a global binary serializer as well as per-type mappers and serializers. Wildcards
are supported for per-type configuration, in which case, the provided configuration will be applied to all types
that match the type name template.
[tabs]
--
tab:XML[]
[source,xml]
----
<bean id="ignite.cfg" class="org.apache.ignite.configuration.IgniteConfiguration">
<property name="binaryConfiguration">
<bean class="org.apache.ignite.configuration.BinaryConfiguration">
<property name="nameMapper" ref="globalNameMapper"/>
<property name="idMapper" ref="globalIdMapper"/>
<property name="typeConfigurations">
<list>
<bean class="org.apache.ignite.binary.BinaryTypeConfiguration">
<property name="typeName" value="org.apache.ignite.examples.*"/>
<property name="serializer" ref="exampleSerializer"/>
</bean>
</list>
</property>
</bean>
</property>
</bean>
----
--
== BinaryObject API
By default, Ignite works with deserialized values as it is the most common use case. To enable `BinaryObject`
processing, a user needs to obtain an instance of `IgniteCache` using the `withKeepBinary()` method. When enabled,
this flag will ensure that objects returned from the cache will be in `BinaryObject` format, when possible. The same
applies to values being passed to the `EntryProcessor` and `CacheInterceptor`.
[NOTE]
====
[discrete]
=== Platform Types
Note that not all types will be represented as `BinaryObject` when the `withKeepBinary()` flag is enabled. There is a
set of 'platform' types that includes primitive types, String, UUID, Date, Timestamp, BigDecimal, Collections,
Maps and arrays of these that will never be represented as a `BinaryObject`.
Note that in the example below key type Integer does not change because it is a platform type.
====
[tabs]
--
tab:Java[]
[source,java]
----
// Create a regular Person object and put it to the cache.
Person person = buildPerson(personId);
ignite.cache("myCache").put(personId, person);
// Get an instance of binary-enabled cache.
IgniteCache<Integer, BinaryObject> binaryCache = ignite.cache("myCache").withKeepBinary();
// Get the above person object in the BinaryObject format.
BinaryObject binaryPerson = binaryCache.get(personId);
----
--
== Modifying Binary Objects Using BinaryObjectBuilder
`BinaryObject` instances are immutable. An instance of `BinaryObjectBuilder` must be used in order to update fields and
create a new `BinaryObject`.
An instance of `BinaryObjectBuilder` can be obtained from `IgniteBinary` facade. The builder may be created using a type
name, in this case the returned builder will contain no fields, or it may be created using an existing `BinaryObject`,
in this case the returned builder will copy all the fields from the given `BinaryObject`.
Another way to get an instance of `BinaryObjectBuilder` is to call `toBuilder()` on an existing instance of a `BinaryObject`.
This will also copy all data from the `BinaryObject` to the created builder.
[NOTE]
====
[discrete]
=== Limitations
* You cannot change the types of existing fields.
* You cannot change the order of enum values or add new constants at the beginning or in the middle of the list of enum's
values. You can add new constants to the end of the list though.
====
Below is an example of using the `BinaryObject` API to process data on server nodes without having user classes deployed
on servers and without actual data deserialization.
[tabs]
--
tab:Java[]
[source,java]
----
// The EntryProcessor is to be executed for this key.
int key = 101;
cache.<Integer, BinaryObject>withKeepBinary().invoke(
key, new CacheEntryProcessor<Integer, BinaryObject, Object>() {
public Object process(MutableEntry<Integer, BinaryObject> entry,
Object... objects) throws EntryProcessorException {
// Create builder from the old value.
BinaryObjectBuilder bldr = entry.getValue().toBuilder();
//Update the field in the builder.
bldr.setField("name", "Ignite");
// Set new value to the entry.
entry.setValue(bldr.build());
return null;
}
});
----
--
== BinaryObject Type Metadata
As it was mentioned above, binary object structure may be changed at runtime hence it may also be useful to get
information about a particular type that is stored in a cache such as field names, field type names, and affinity
field name. Ignite facilitates this requirement via the `BinaryType` interface.
This interface also introduces a faster version of field getter called `BinaryField`. The concept is similar to java
reflection and allows to cache certain information about the field being read in the `BinaryField` instance, which is
useful when reading the same field from a large collection of binary objects.
[tabs]
--
tab:Java[]
[source,java]
----
Collection<BinaryObject> persons = getPersons();
BinaryField salary = null;
double total = 0;
int cnt = 0;
for (BinaryObject person : persons) {
if (salary == null)
salary = person.type().field("salary");
total += salary.value(person);
cnt++;
}
double avg = total / cnt;
----
--
== BinaryObject and CacheStore
Setting `withKeepBinary()` on the cache API does not affect the way user objects are passed to a `CacheStore`. This is
intentional because in most cases a single `CacheStore` implementation works either with deserialized classes, or with
`BinaryObject` representations. To control the way objects are passed to the store, the `storeKeepBinary` flag on
`CacheConfiguration` should be used. When this flag is set to `false`, deserialized values will be passed to the store,
otherwise `BinaryObject` representations will be used.
Below is an example pseudo-code implementation of a store working with `BinaryObject`:
[tabs]
--
tab:Java[]
[source,java]
----
public class CacheExampleBinaryStore extends CacheStoreAdapter<Integer, BinaryObject> {
@IgniteInstanceResource
private Ignite ignite;
/** {@inheritDoc} */
@Override public BinaryObject load(Integer key) {
IgniteBinary binary = ignite.binary();
List<?> rs = loadRow(key);
BinaryObjectBuilder bldr = binary.builder("Person");
for (int i = 0; i < rs.size(); i++)
bldr.setField(name(i), rs.get(i));
return bldr.build();
}
/** {@inheritDoc} */
@Override public void write(Cache.Entry<? extends Integer, ? extends BinaryObject> entry) {
BinaryObject obj = entry.getValue();
BinaryType type = obj.type();
Collection<String> fields = type.fieldNames();
List<Object> row = new ArrayList<>(fields.size());
for (String fieldName : fields)
row.add(obj.field(fieldName));
saveRow(entry.getKey(), row);
}
}
----
--
== Binary Name Mapper and Binary ID Mapper
Internally, Ignite never writes full strings for field or type names. Instead, for performance reasons, Ignite writes
integer hash codes for type and field names. Testing has indicated that hash code conflicts for the type names or the
field names within the same type are virtually non-existent and, to gain performance, it is safe to work with hash codes.
For the cases when hash codes for different types or fields actually do collide, `BinaryNameMapper` and `BinaryIdMapper`
support overriding the automatically generated hash code IDs for the type and field names.
`BinaryNameMapper` - maps type/class and field names to different names.
`BinaryIdMapper` - maps given from `BinaryNameMapper` type and field name to ID that will be used by Ignite in internals.
Ignite provides the following out-of-the-box mappers implementation:
* `BinaryBasicNameMapper` - a basic implementation of `BinaryNameMapper` that returns a full or a simple name of a given
class depending on whether the `setSimpleName(boolean useSimpleName)` property is set.
* `BinaryBasicIdMapper` - a basic implementation of `BinaryIdMapper`. It has a configuration property called
`setLowerCase(boolean isLowerCase)`. If the property is set to `false` then a hash code of given type or field name
will be returned. If the property is set to `true` then a hash code of given type or field name in lower case will be returned.
If you are using Java or .NET clients and do not specify mappers in `BinaryConfiguration`, then Ignite will use
`BinaryBasicNameMapper` and the `simpleName` property will be set to `false`, and `BinaryBasicIdMapper` and the
`lowerCase` property will be set to `true`.
If you are using the C{pp} client and do not specify mappers in `BinaryConfiguration`, then Ignite will use
`BinaryBasicNameMapper` and the `simpleName` property will be set to `true`, and `BinaryBasicIdMapper` and the
`lowerCase` property will be set to `true`.
By default, there is no need to configure anything if you use Java, .NET or C{pp}. Mappers need to be configured if
there is a tricky name conversion when platform interoperability is needed.