Say I have a class structure as follows, it is pretty basic inheritance:
Manager extends Person {
private String name;
Manager() {
}
}
Clerk extends Person {
private String salary;
}
In spring Data if I store these in Mongo, is it possible to configure it to map the correct class when I do a getById. I assume i will have to store some class info?
What i dont want to do is the need to create seperate repository classes if i can avoid it, also i dont know what the object will be when i do a getById
If you are using spring-data-mongodb MongoRepository to write data in your database according to your entity model, a _class field will be added to document roots and to complex property types (see this section). This fields store the fully qualified name of the Java class and it allows disambiguation when mapping from MongoDb Document to Spring data model.
However, if you only use MongoRepository to read from your database, you need to tell Spring-data how to map your entities explicitly. You will need to Override Mapping with Explicit Converters.
PersonReadConverter.class
public class PersonReadConverter implements Converter<Document, Person> {
#Override
public Contact convert(Document source) {
if (source.get("attribute_specific_to_Clerk") != null) {
Clerk clerk = new Clerk();
//Set attributes using setters or defined constructor
return clerk;
}
else {
Manager manager = new Manager()
//Set attribute using setters or defined constructor
return manager;
}
}
}
Then, you have to Register Spring Converters with the MongoConverter.
You can find an example of my own at: Spring Data Mongo - How to map inherited POJO entities?
Related
I'm using spring-data-mongodb at the moment so this question is primarily in context of MongoDB but I suspect my question applies to repository code in general.
Out of the box when using a MongoRepository<T, ID> interface (or any other Repository<T, ID> descendent) the entity type T is expected to be the document type (the type that defines the document schema).
As a result injecting such a repository into service component means this repository is leaking database schema information into the service tier (highly pseudo) :
class MyModel {
UUID id;
}
#Document
class MyDocument {
#Id
String id;
}
interface MyRepository extends MongoRepository<MyDocument, String> {
}
class MyService {
MyRepository repository;
MyModel getById(UUID id) {
var documentId = convert(id, ...);
var matchingDocument = repository.findById(documentId).orElse(...);
var model = convert(matchignDocument, ...);
return model;
}
}
Whilst ideally I'd want to do this :
class MyModel {
UUID id;
}
#Document
class MyDocument {
#Id
String id;
}
#Configuration
class MyMagicConversionConfig {
...
}
class MyDocumentToModelConverter implements Converter<MyModel, MyDocument> {
...
}
class MyModelToDocumentConverter implements Converter<MyDocument, MyModel> {
...
}
// Note that the model and the model's ID type are used in the repository declaration
interface MyRepository extends MongoRepository<MyModel, UUID> {
}
class MyService {
MyRepository repository;
MyModel getById(UUID id) {
// Repository now returns the model because it was converted upstream
// by the mongo persistence layer.
var matchingModel = repository.findById(documentId).orElse(...);
return matchingModel ;
}
}
Defining this conversion once seems significantly more practical than having to consistently do it throughout your service code so I suspect I'm just missing something.
But of course this requires some way to inform the mongo mapping layer to be aware of what conversion has to be applied to move between MyModel and MyDocument and to use the latter for it's actual source of mapping metadata (e.g. #Document, #Id, etc.).
I've been fiddling with custom converters but I just can't seem to make the MongoDB mapping component do the above.
My two questions are :
Is it currently possible to define custom converters or implement callbacks that allow me to define and implement this model <-> document conversion once and abstract it away from my service tier.
If not, what is the idiomatic way to approach cleaning this up such that the service layer can stay blissfully unaware of how or with what schema an entity is persisted? A lot of Spring Boot codebases appear to be fine with using the type that defines the database schema as their model but that seems supoptimal. Suggestions welcome!
Thanks!
I think you're blowing things a bit out of proportion. The service layer is not aware of the schema. It is aware of the types returned by the repository. How the properties of those are mapped onto the schema, depends on the object-document mapping. This, by default, uses the property name, as that's the most straightforward thing to do. That translation can either be customized using annotations on the document type or by registering a FieldNamingStrategy with Spring Data MongoDB.
Spring Data MongoDB's object-document mapping subsystem provides a lot of customization hooks that allows transforming arbitrary MongoDB documents into entities. The types which the repositories return are your domain objects that - again, only by default - are mapped onto a MongoDB document 1:1, simply because that's the most reasonable thing to do in the first place.
If really in doubt, you can manually implement repository methods individually that allow you to use the MongoTemplate API that allows you to explicitly define the type, the data should be projected into.
You can use something like MapStruct or write your own Singleton Mapper.
Then create default methods in your repository:
interface DogRepository extends MongoRepository<DogDocument, String> {
DogDocument findById(String id);
default DogModel dogById(String id) {
return DogMapper.INSTANCE.toModel(
findById(id)
);
}
}
Since I want to use ObservableLists in my entities classes within JavaFX, originally I had a problem with the dedicated List implementation and injection over reflection that Hibernate is using by default. Therefore I decided to have my Entity classes annotated with #Access(AccessType.PROPERTY), because I want to enforce that Hibernate uses my getter and setter methods and not reflection.
In a particular class I have a number of List attributes e.g. protected List<CostEstimate> costEstimates;. For each of these lists I have getters and setters which are annotated accordingly. So far so good, that seems to work.
The trouble is that in my UI I don't want to show e.g. all costEstimates that were created over time, but only the last one. So I created a method public CostEstimate getLastCostEstimate() which would return only the last element from the List. This method is annotated with #Transient since there is no matching column in the MySQL database, since it only returns the last element from the related list.
My controller class binds getLastCostEstimate() of the entity to the according UI element.
In the default constructor of my entity class the costEstimates list is initialized with a initial default estimate, such that getLastCostEstimate() should always return a meaningful CostEstimate. In the debugger I can see that this initialization is executed. However, at run time the costEstimates list is empty and I get an IndexOutOfBoundsException. I assume that has to do with the #Transient annotation ?! I wonder whether I have a coding or design issue? I guess my question is: how to model this "give me only the last element from a list" in a JPA entity class (without too much boilerplate code)?
Thank you for your help!
For your convenience please find following some related code snippets:
#Entity
#Inheritance
#Access(AccessType.PROPERTY) // JPA reading and writing attributes through their accessor getter and setter methods
public abstract class UserRequirement extends Requirement implements Serializable {
..
protected List<CostEstimate> costEstimates; // Money needed
..
protected UserRequirement() {
..
costEstimates = FXCollections.observableArrayList();
setLastCostEstimate(new CostEstimate(0));
..
}
..
#OneToMany(mappedBy="parent", orphanRemoval = true, cascade = CascadeType.PERSIST, fetch = FetchType.LAZY)
public List<CostEstimate> getCostEstimates() {
return costEstimates;
}
#SuppressWarnings("unused") // Used by JPA
public void setCostEstimates(List<CostEstimate> newCostEstimates) {
if(newCostEstimates != null) {
((ObservableList<CostEstimate>)costEstimates).setAll(newCostEstimates);
} else {
costEstimates.clear();
}
}
#Transient
public CostEstimate getLastCostEstimate() {
return costEstimates.get(costEstimates.size()-1);
}
public void setLastCostEstimate(CostEstimate costEstimate) {
if(costEstimate = null) {
return;
}
costEstimates.add(costEstimate);
}
..
}
We have implemented an application that should be able to use either JPA, Couchbase or MongoDB. (for now, may increase in the future). We successfully implemented JPA and Couchbase by separating repositories for each e.g. JPA will come from org.company.repository.jpa while couchbase will come from org.company.repository.cb. All repository interfaces extends a common repository found in org.company.repository. We are now targeting MongoDB by creating a new package org.company.repository.mongo. However we are encountering this error:
No property updateLastUsedDate found for type TokenHistory!
Here are our codes:
#Document
public class TokenHistory extends BaseEntity {
private String subject;
private Date lastUpdate;
// Getters and setters here...
}
Under org.company.repository.TokenHistoryRepository.java
#NoRepositoryBean
public interface TokenHistoryRepository<ID extends Serializable> extends TokenHistoryRepositoryCustom, BaseEntityRepository<TokenHistory, ID> {
// No problem here. Handled by Spring Data
TokenHistory findBySubject(#Param("subject") String subject);
}
// The custom method
interface TokenHistoryRepositoryCustom {
void updateLastUsedDate(#Param("subject") String subject);
}
Under org.company.repository.mongo.TokenHistoryMongoRepository.java
#RepositoryRestResource(path = "/token-history")
public interface TokenHistoryMongoRepository extends TokenHistoryRepository<String> {
TokenHistory findBySubject(#Param("subject") String subject);
}
class TokenHistoryMongoRepositoryCustomImpl {
public void updateLastUsedDate(String subject) {
//TODO implement this
}
}
And for Mongo Configuration
#Configuration
#Profile("mongo")
#EnableMongoRepositories(basePackages = {
"org.company.repository.mongo"
}, repositoryImplementationPostfix = "CustomImpl",
repositoryBaseClass = BaseEntityRepositoryMongoImpl.class
)
public class MongoConfig {
}
Setup is the same for both JPA and Couchbase but we didn't encountered that error. It was able to use the inner class with "CustomImpl" prefix, which should be the case base on the documentations.
Is there a problem in my setup or configuration for MongoDB?
Your TokenHistoryMongoRepositoryCustomImpl doesn't actually implement the TokenHistoryRepositoryCustom interface, which means that there's no way for us to find out that updateLastUsedDate(…) in the class found is considered to be an implementation of the interface method. Hence, it's considered a query method and then triggers the query derivation.
I highly doubt that this works for the other stores as claimed as the code inspecting query methods is shared in DefaultRepositoryInformation.
How can I parameterize a SpringData ElasticSearch index at runtime?
For example, the data model:
#Document(indexName = "myIndex")
public class Asset {
#Id
public String id;
// ...
}
and the repository:
public interface AssetRepository extends ElasticsearchCrudRepository<Asset, String> {
Asset getAssetById(String assetId);
}
I know I can replace myIndex with a parameter, but that parameter will be resolved during instantiation / boot. We have the same Asset structure for multiple clients / tenants, which have their own index. What I need is something like this:
public interface AssetRepository extends ElasticsearchCrudRepository<Asset, String> {
Asset getAssetByIdFromIndex(String assetId, String index);
}
or this
repoInstance.forIndex("myOtherIndex").getAssetById("123");
I know this does not work out of the box, but is there any way to programmatically 'hack' it?
Even though the bean is init at boot time, you can still achieve it by spring expression language:
#Bean
Name name() {
return new Name();
}
#Document(indexName="#{name.name()}")
public class Asset{}
You can change the bean's property to change the index you want to save/search:
assetRepo.save(new Asset(...));
name.setName("newName");
assetRepo.save(new Asset(...));
What should be noticed is not to share this bean in multiple thread, which may mess up your index.
Here is a working example.
org.springframework.data.elasticsearch.repository.ElasticSearchRepository has a method
FacetedPage<T> search(SearchQuery searchQuery);
where SearchQuery can take multiple indices to be used for searching.
I hope it answers
I'm trying to use GWT + Spring + Hibernate
When lunshing the application I get this error:
com.google.gwt.user.client.rpc.SerializationException: Type 'org.hibernate.collection.PersistentBag' was not included in the set of types which can be serialized by this SerializationPolicy or its Class object could not be loaded. For security purposes, this type will not be serialized.: instance = [com.asso.shared.model.Activite#64d6357a]
after using this method with the lists of the persistence classes:
public static <T> ArrayList<T> makeGWTSafe(List<T> list) {
if(list instanceof ArrayList) {
return (ArrayList<T>)list;
} else {
ArrayList<T> newList = new ArrayList<T>();
newList.addAll(list);
return newList;
}
}
with my lists I got this:
com.google.gwt.user.client.rpc.SerializationException: Type 'org.hibernate.collection.PersistentBag' was not included in the set of types which can be serialized by this SerializationPolicy or its Class object could not be loaded. For security purposes, this type will not be serialized.: instance = [com.asso.shared.model.Personne#75a2fb58]
==========================================
I have searched in the other subjects but I can't find any solution!
How can I solve this serialization thing!?
I'm using List in my Persistence classes
You need to send DTO object to client side (instead of original one backed by Hibernate). The problem is that your Personne object is actually a Hibernate proxy. Each time when you call some method on it Hibernate do some work (fetch collections from DB for example). There is no simple way to serialize such kind of objects.
Hibernate entities:
//Hibernate entity
public class Personne {
private String name;
private List<Address> addresses;
}
//Hibernate entity
public class Address {
}
Corresponding DTO objects:
public class PersonneDto {
private String name;
private List<AddressDto> addresses;
}
public class AddressDto {
}
Instead of sending Personne to client side you need to create new PersonneDto object, copy state to it and then send to UI. Personne cannot be used in client side because Personne.getAddresses() in most cases hit DB to fetch data (which is inpossible to do in client side JS). So each Personne must be replaced by PersonneDto on client side. As a downside you need to mantnain additional layaer of DTO objects and corresponding code to transform entities to DTOs. There are another approaches to this problem. See this article for more details.