-
Notifications
You must be signed in to change notification settings - Fork 684
Developer guide
The Spring Data Commons module provides a sophisticated system to gather and use entity mapping and conversion functionality. The core model abstractions are PersistentEntity
, PersistentProperty
. These abstractions can be created and used through a MappingContext
. In top of that we provide an EntityConverter
abstraction consisting of EntityReader
and EntityWriter
.
TODO
As an important part of the entity conversion on the reading side is creating instances of the domain class an EntityInstantiator
API allows plugging custom code for
By default the conversion system inspects the properties of the type being read to find out which nested types it has to instantiate. So assume the following domain classes:
class Address { … }
class Person {
Address address;
}
If the domain model works with type hierarchies, inspecting the property types might not be sufficient to read data into an object graph. Assume the domain model slightly modified and a Person
instance created as follows:
class Address { … }
class Person {
Object address;
}
Person person = new Person();
person.address = new Address();
In this case there are two important things to consider: first, the type information of the address
has to be stored when persisting the Person
instance. Second, when reading the object we need to detect the type to be instantiated for the data eventually forming the Address
instance.
To achieve this, Spring Data Commons provides the TypeMapper
API. The interface has to be typed to the actual store's native data representation (e.g. a DBObject
in MongoDB). It's core responsibility is writing type information to the store object and read it back in in turn. We also ship a configurable DefaultTypeMapper
that takes a TypeAliasAccessor
, a MappingContext
as well as TypeInformationMapper
implementations to delegate to.
The TypeAliasAccessor
is the core interface to implement how a type alias gets actually persisted to the store (through writeTypeTo(…)
)and read back in (through readTypeAlias(…)
). It is not responsible to interpret the token written, it's solely encapsulating the reading and writing aspect and has to be implemented in a store-specific way.
The type alias is some arbitrary token which can be mapped to a Java type in a unique way. This can be the fully-qualified Java type name or another unique hash-like token.
The TypeInformationMapper
is responsible for mapping the arbitrary alias into a TypeInformation
instance, i.e. actually resolving the type. Spring Data Commons ships with a variety of implementations for that out of the box:
-
SimpleTypeInformationMapper
- stores the fully-qualified class name on writing and interpreting the token returned by theTypeAliasAccessor
as class name and trying to load the class. -
MappingContextTypeInformationMapper
- uses aMappingContext
which inspects the entities persisted for the@TypeAlias
annotation. It will return the value configured if annotated ornull
if no type alias information is present with thePersistentEntity
(seePersistentEntity#getTypeAlias()
) -
ConfigurableTypeInformationMapper
- takes aMap<? extends Class<?>, String>
to register a manual mapping from a type to a given alias.
These implementations can be configured as a chain on the DefaultTypeMapper
. The class will then consult them in a row using the alias or type of the one returning a non-null
value. This allows some types being mapped into aliases by @TypeAlias
whereas all others fall back on the fully-qualified class name.
However, there a a few constructors on the DefaultTypeMapper
that make the setup quite easy. The one taking a TypeAliasAccessor
registers a SimpleTypeInformationMapper
by default. Assume we have a TypeAliasAccessor
for a Map
like this:
class MapTypeAliasAccessor implements TypeAliasAccessor<Map<String, Object>> {
public static final String TYPE_ALIAS_KEY = "_class";
public Object readAliasFrom(Map<String, Object> source) {
return source.get(TYPE_ALIAS_KEY);
}
public void writeTypeTo(Map<String, Object> sink, Object alias) {
sink.put(TYPE_ALIAS_KEY, alias);
}
}
We could the setup a DefaultTypeMapper
instance and use it as follows:
MapTypeAliasAccessor accessor = new MapTypeAliasAccessor();
TypeMapper<Map<String, Object> mapper = new DefaultTypeMapper<Map<String, Object>(accessor);
Map<String, Object> store = new HashMap<String, Object>();
mapper.writeType(HashMap.class, store);
// Make sure we have the type information captured
assertThat(store.get(TYPE_ALIAS_KEY), is(HashMap.class.getName()));
// Make sure we can obtain the type from the plain store source
assertThat(mapper.readType(store), is(ClassTypeInformation.from(HashMap.class)));
If we hand it a MappingContext
and our Address
is annotated with @TypeAlias("A")
, then the DefaultTypeMapper
would work as follows:
MapTypeAliasAccessor accessor = new MapTypeAliasAccessor();
MappingContext<?, ?> context = … // obtain MappingContext
TypeMapper<Map<String, Object> mapper = new DefaultTypeMapper<Map<String, Object>(accessor, context, Collections.emptyList());
Map<String, Object> store = new HashMap<String, Object>();
mapper.writeType(Address.class, store);
// Make sure the alias is written, not the class name
assertThat(store.get(TYPE_ALIAS_KEY, is("A")));
// Make sure we discover Address to be the target ty
assertThat(mapper.readType(store), is(ClassTypeInformation.from(Address.class)));
Note, that storing type information for Person
would still store the fully-qualified class name as the MappingContext
does not find any type alias mapping information.
Usually EntityConverter
implementations will setup a DefaultTypeMapper
in their constructors and hand in a MappingContext
so that type information is transparently stored as fully-qualified class names and consider the customization via @TypeAlias
out-of-the box. The only store specific part is the TypeAliasAccessor
implementation, which has to be adapted according to the store's data structure. Have a look at MongoDB's DBObjectTypeAliasAccessor
for example.
Beyond that it's good practice to expose the TypeMapper
as configurable property of the EntityConverter
implementation so that uses gain full control over the type mapping setup if necessary.
We provide a set of repository interfaces that either declare a user's repository interface as a Spring Data interface or even pull in functionality that can be implemented generically.
-
Repository
- A plain marker interface to let the Spring Data infrastructure pick up user defined repositories. -
CrudRepository
- ExtendsRepository
and adds basic persistence methods like saving entities, finding entities and deleting them. -
PagingAndSortingRepositories
- ExtendsCrudRepository
and adds method for access ing entities page by page and sorted by a given criteria.
TODO
When building a store implementation for a data store we do not already support the most interesting parts are the mapping and conversion system as well as the repository abstraction. If the store you target already supports entity mapping (like JPA for example) you can implement the repository abstraction directly on top of it. Otherwise you need to integrate with the mapping and conversion system. The following sections will describe the important abstractions and classes you'll have to take a look and and extend/implement.
As example for an implementation of store support with Spring Data mapping have a look at Spring Data MongoDB, for a plain repository abstraction integration consider taking a look at Spring Data JPA.
TODO
The very core of the repository abstraction is the factory to create repository instances. RepositoryFactorySupport
requires the following methods to be implemented:
-
getEntityInformation(…)
- return theEntityInformation
which encapsulates ways to determine whether an entity is new, lookup the identifier of the entity as well as the type of the id.AbstractEntityInformation
is the class you'll probably want to extend. -
getRepositoryBaseClass(…)
- returns the type of the backing instance of the repositories which usually implements CRUD methods etc. Needed to inspect the user's repository interface for query methods before actually creating the instance. -
getTargetRepository(…)
- returns an instance of the type returned bygetRepositoryBaseClass(…)
. This instance will usually return one of the Spring Data Repository interfaces.
TODO
TODO