Constraints
The GigaMap can validate every write operation (add, set, update, …) against a set of constraints. If any constraint is violated, a ConstraintViolationException is thrown and the operation is rolled back, so the map is never left in an invalid state.
Constraints are accessed through gigaMap.constraints(), which returns a GigaConstraints instance with two categories:
gigaMap.constraints().unique(); // UniqueConstraints<E>
gigaMap.constraints().custom(); // CustomConstraints<E>
Unique Constraints
A unique constraint ensures that the indexed value of a property is unique across all entities in the map. It is backed by a bitmap index, so adding one always implies that a corresponding bitmap index exists.
// use the builder
final GigaMap<Person> gigaMap = GigaMap.<Person>Builder()
.withBitmapUniqueIndex(PersonIndices.id)
...
.build();
// or register it after creating
GigaMap<Person> gigaMap = GigaMap.New();
gigaMap.index().bitmap().addUniqueConstraint(PersonIndices.id);
Combining With an Identity Index
A unique constraint and an identity index are independent concerns: the identity index defines how entities are looked up, the unique constraint enforces uniqueness at write time. Combine both to get an identifying field that is also guaranteed unique:
// use the builder — the same indexer can be passed to both methods
final GigaMap<Person> gigaMap = GigaMap.<Person>Builder()
.withBitmapIdentityIndex(PersonIndices.id)
.withBitmapUniqueIndex(PersonIndices.id) // enforce uniqueness at runtime
...
.build();
// or register it after creating: addUniqueConstraint creates the backing
// bitmap index, then setIdentityIndices promotes it to identity
GigaMap<Person> gigaMap = GigaMap.New();
final BitmapIndices<Person> bitmap = gigaMap.index().bitmap();
bitmap.addUniqueConstraint(PersonIndices.id); // creates index + unique constraint
bitmap.setIdentityIndices(PersonIndices.id); // promote it to identity
Custom Constraints
Custom constraints let you express arbitrary validation logic — anything from a simple per-entity predicate to a check that consults the rest of the map.
|
Custom constraints are persisted with the GigaMap: when the map is stored, the registered constraint instances are written along with it and are restored when the map is loaded again. Because of this, every constraint must be implemented as a named, top-level (or static nested) class. EclipseStore cannot persist:
In practice this means: only use |
A custom constraint implements CustomConstraint<E>. There are four ready-made base types, ordered from simplest to most flexible:
| Type | When to use |
|---|---|
|
Validation depends only on the new entity itself. |
|
Validation also needs the |
|
Same as |
|
Same as |
Adding Constraints
// use the builder
final GigaMap<Person> gigaMap = GigaMap.<Person>Builder()
.withCustomConstraint(new NoBlankNames())
...
.build();
// or register one or more after creating
gigaMap.constraints().custom().addConstraint(new NoBlankNames());
gigaMap.constraints().custom().addConstraints(constraint1, constraint2);
When a custom constraint is added to a non-empty GigaMap, it is immediately checked against every existing entity. If any current entity already violates it, the call fails with a ConstraintViolationException and the constraint is not registered.
|
Example: AbstractSimple
The most common case — a per-entity rule:
public class NoBlankNames extends CustomConstraint.AbstractSimple<Person>
{
@Override
public boolean isViolated(final Person person)
{
return person.getFirstName() == null || person.getFirstName().isBlank();
}
}
Example: WrapperSimple (lambda)
Same logic, expressed as a predicate:
gigaMap.constraints().custom().addConstraint(
new CustomConstraint.WrapperSimple<Person>(
person -> person.getFirstName() == null || person.getFirstName().isBlank()
)
);
The wrapped Predicate is a lambda and cannot be persisted by EclipseStore. Use this form only for in-memory GigaMaps. For persisted maps, port the logic to a named subclass of AbstractSimple (see above).
|
Example: Abstract
When the check needs the entity id or the replaced entity, declare a named subclass:
public class NoLevelDowngrade extends CustomConstraint.Abstract<Person>
{
@Override
public void check(final long entityId, final Person replaced, final Person entity)
{
if(replaced != null && entity.getLevel() < replaced.getLevel())
{
throw new ConstraintViolationException(entityId, replaced, entity);
}
}
}
gigaMap.constraints().custom().addConstraint(new NoLevelDowngrade());
For persisted GigaMaps, prefer named classes over the new CustomConstraint.Abstract<>() { … } anonymous-class shorthand. Anonymous-inner-class names (…$1, …$2) depend on declaration order in the enclosing class and are fragile across refactors.
|
Example: Wrapper (full Logic lambda)
When the check needs to consult the rest of the map:
final CustomConstraint.Wrapper<Person> singleAdmin = new CustomConstraint.Wrapper<>(
(map, entityId, replaced, entity, createException) ->
{
if(!entity.isAdmin())
{
return null;
}
final long admins = map.query(PersonIndices.admin.is(true)).count();
if(admins == 0 || (replaced != null && replaced.isAdmin()))
{
return null;
}
return createException
? new ConstraintViolationException(entityId, replaced, entity)
: org.eclipse.serializer.util.X.BREAK();
}
);
gigaMap.constraints().custom().addConstraint(singleAdmin);
The wrapped Logic is a lambda and cannot be persisted by EclipseStore. Use this form only for in-memory GigaMaps. For persisted maps, port the logic to a named subclass of Abstract (see above) — the check method has access to the same entityId and replaced parameters; if you also need the surrounding GigaMap, implement CustomConstraint<E> directly.
|
Constraint Names
Each CustomConstraint has a name() used as its registry key — duplicate names are rejected with an IllegalArgumentException. The default name() in AbstractBase returns the simple class name, which is sufficient for named subclasses (the recommended form, see the persistence note above). Anonymous subclasses fall back to an empty simple name, so registering more than one without overriding name() will fail.
Constraint Violations
All violations are signalled by ConstraintViolationException (or its subclass UniqueConstraintViolationException for unique-index violations). The exception carries:
-
entityId— the id of the offending entity -
replacedEntity— the previous entity forset/update,nullforadd -
violatingEntity— the entity that triggered the violation
What happens to the map on violation depends on the operation:
| Operation | Effect on the map |
|---|---|
|
The map is unchanged. For batch operations none of the entities in the batch are added. |
|
The map is unchanged. Constraints are checked before the entry is mutated, so the previous entity stays in place. |
|
The offending entity is removed from the map. The provided logic mutates the entity in place, so a constraint failure cannot be rolled back to the previous state — the only consistent option is to drop the entry. The removed entity and its id are carried by the |