I am building a profile service with the typical REST endpoints for creating, reading, updating and deleting profiles. For this I am using the Spring Framework together with a MongoDB. On top I would like to use QueryDSL to create some custom queries.
A full minimal working example of the current implementation can be found here: https://github.com/mirrom/profile-modules
I would like to have sub profile models that extend the base profile model, and sub sub models that extend the sub models. By this I have hierarchical profiles that inherit the fields of its parent profile. The idea is to store all profiles in the same collection and distinguish them via the automatically created _class field.
A simple example (with Lombok annotations):
#Data
#Document(collection = "profiles")
#Entity
public class Profile {
#Id
private ObjectId id;
#Indexed
private String title;
#Indexed
private String description;
private LocalDateTime createdAt;
private LocalDateTime modifiedAt;
}
#Data
#Entity
#EqualsAndHashCode(callSuper = true)
public class Sub1Profile extends Profile {
private String sub1String;
private int sub1Integer;
}
While (all) profiles can get accessed via the endpoint /api/v1/profiles, the sub1Profiles can be accessed via /api/v1/profiles/sub-1-profiles. Currently the sub1Profiles endpoint delivers all profiles, but it should just deliver the sub1Profiles and its children. For this I would like to use QueryDSL, but I can't add QuerydslPredicateExecutor<Profile> and QuerydslBinderCustomizer<QProfile> to more than one repository interface. This is how my profile repository looks like:
#Repository
public interface ProfileRepository extends MongoRepository<Profile, ObjectId>, QuerydslPredicateExecutor<Profile>,
QuerydslBinderCustomizer<QProfile> {
#Override
default void customize(QuerydslBindings bindings, QProfile root) {
bindings.bind(String.class)
.first((SingleValueBinding<StringPath, String>) StringExpression::containsIgnoreCase);
}
}
If I now try to do the same with Sub1ProfileRepository:
#Repository
public interface Sub1ProfileRepository
extends MongoRepository<Sub1Profile, ObjectId>, QuerydslPredicateExecutor<Sub1Profile>,
QuerydslBinderCustomizer<QSub1Profile> {
default void customize(QuerydslBindings bindings, QProfile root) {
bindings.bind(String.class)
.first((SingleValueBinding<StringPath, String>) StringExpression::containsIgnoreCase);
}
}
I get this error message:
org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'sub1ProfileRepository' defined in com.example.profile.repository.sub1profile.Sub1ProfileRepository defined in #EnableMongoRepositories declared on MongoRepositoriesRegistrar.EnableMongoRepositoriesConfiguration: Invocation of init method failed; nested exception is org.springframework.data.mapping.PropertyReferenceException: No property customize found for type Sub1Profile!
What am I missing?
In Sub1ProfileRepository's customize method, you have used QProfile as method argument. Can you use QSub1Profile instead and check if it's working?
Related
I'm creating dynamic collections by using mongoTemplate in service layer. Upto this everything went well but when saving into collection dynamically makes issue. explaining here...
Service Layer
public void createCollection(String collectionName) {
mongoTemplate.createCollection(collectionName);
}
public Object updateLessonOrSurveyOrQuery(String courseID, int levelNo, CourseAsset courseAssetToUpdate) {
.....
courseAssetRepo.saveByCourseID(courseID, courseAssetToUpdate);
.....
}
Repo Layer
#Repository
public interface CourseAssetRepo extends MongoRepository<CourseAsset, String> {
ArrayList<CourseAsset> findAllByCourseID(String courseID);
void saveByCourseID( String courseID, CourseAsset courseAsset);
}
findAllByCourseID working but saveByCourseID not woking;
POJO class
#Data
public class CourseAsset {
private int level;
private String title;
private String courseID;
}
ERROR :
Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'courseAssetRepo' defined in com.dotbw.learn.repo.CourseAssetRepo defined in #EnableMongoRepositories declared on MongoRepositoriesRegistrar.EnableMongoRepositoriesConfiguration: Could not create query for public abstract void com.dotbw.learn.repo.CourseAssetRepo.saveByCourseID(java.lang.String,com.dotbw.learn.model.CourseAsset); Reason: No property 'saveByCourseID' found for type 'CourseAsset'
i can understand repo expects CourseAsset Data inside the pojo class. But while saving how we can provide this value.
i have tried many way as ChatGPT said but nothing worked.
Steps I am following
I write Swagger File for API contract and Definition models for my Microservice
Now I am using Swagger codegen dependencies in my Spring boot application to generate Models from reading the Swagger from URL where it is hosted in target (output directory mentioned in pom.xml) directory of my application on "mvn install".
For each definition in the Swagger file One model class will be generated in my application's target directory.
Now I am using mongoDB as database for models to save as collection.
Need to Give #Document(value = "collection-name") - dynamically for model classes.
As model class are generated through Swagger codegen cant edit those,
So how to Maintain Dynamic Name for definitions which I want to save in DB
Is there any way through Swagger Contract to define this ?
A while ago I was struggling with this same problem, and some solutions include post-processing the generated files.
However, as a security concern, exposing the MongoDB model to the API might not be completely right. An interesting approach would be to simply use Mappers and apply the DTO concept to the auto-generated model classes.
For instance:
Suppose you have a users collection in MongoDB, and an API to retrieve the users.
components:
schemas:
User:
type: object
properties:
id:
type: string
name:
type: string
The Swagger auto-generated class would be something like this:
package api;
#ApiModel()
#Validated
public class User {
#JsonProperty("id")
private String id = null;
#JsonProperty("name")
private String name = null;
}
You will also have the 'standard' User model class:
package model;
#Document(collection = "users")
public class User {
#Id
private String id;
#NotNull
private String name;
#NotNull
private String password;
}
Notice for example, that the password field is not included in the API definition.
Then we'll have the typical User repository:
public interface UserRepository extends MongoRepository<User, String> { }
Now we have to implement the mapper, which is very straightforward using MapStruct:
#Mapper(componentModel = "spring", unmappedTargetPolicy = ReportingPolicy.IGNORE)
public interface UserMapper {
api.User map(model.User user);
}
You can widely customize which and how to map the information.
The only thing remaining is the controller:
#Controller
public class MyController implements MyApi {
#Inject
UserMapper mapper;
#Autowired
private UserRepository repository;
#GetMapping("/api/v1.0/users")
public ResponseEntity<List<api.User>> getAllUsers() {
List<api.User> users = new ArrayList<>();
repository.findAll().forEach(user -> users.add(mapper.map(user)));
return new ResponseEntity(users, HttpStatus.OK);
}
I am currently working on a Spring Boot project and I would like to speed up process of writing the service/data layer boilerplate code (one service and one repository (CrudRepository) for every entity, every one having mostly the same methods).
As of now I am using TABLE_PER_CLASS inheritance in several entities (e.g.: Warehouse and Office are subclasses of Location (an abstract class defining common attributes for all locations).
I would like to define 1 repository and 1 service to manage both Location and its subtypes so I can do something like in my control layer:
#Autowired
LocationsService locationsService;
Warehouse cityWarehouse = new Warehouse();
Office centralOffice = new Office();
locationsService.addNewLocation(cityWarehouse);
locationsService.addNewLocation(centralOffice);
I know I can just use method overloading but I would really like to avoid repeating the same code in situations like this one.
I've also tried using parametric polymorphism:
#Service
public class LocationsService {
#Autowired
LocationsRepository locationsRepository;
public void addNewLocation(Location location) {
locationsRepository.save(location);
}
}
Unfortunately this won't work as Spring can't tell if I want to save a Location or a Warehouse object:
nested exception is org.springframework.orm.jpa.JpaObjectRetrievalFailureException:
Unable to find com.test.springboot.entities.locations.Location with id 55db6993-8a58-4e3a-a6ab-d60d93ab6182; nested exception is javax.persistence.EntityNotFoundException: Unable to find com.test.springboot.entities.locations.Location with id 55db6993-8a58-4e3a-a6ab-d60d93ab6182
I need to use concrete Location objects so using #MappedSuperclass is not an option.
Is there something I am missing? Is it even posible to achieve what I want?
Please note that I am fairly new to Spring Boot so maybe there's something obvious I don't know about yet.
I got it working thanks to some of the comments and after briefly reading the JPA specification.
Because I wanted to use my superclasses as entities I ended up using SINGLE_TABLE inheritance.
For example, this is the Location entity:
#Entity
#Data
#Accessors(chain = true)
#Inheritance(strategy = InheritanceType.SINGLE_TABLE)
#DiscriminatorColumn(name = "location_type")
public class Location {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
// Skipped
}
The key here is to use #DiscriminatorColumn with SINGLE_TABLE inheritance in the parent class and add #DiscriminatorValue in the correspondent subclasses:
#Entity
#Data #Accessors(chain = true)
#DiscriminatorValue("warehouse_location")
public class Warehouse extends Location {
#JoinColumn(name = "INTERNAL_ROUTE_ID")
#OneToOne(orphanRemoval = true)
private Route internalRoute;
#JoinColumn(name = "EXTERNAL_WAREHOUSE_ID")
#OneToMany(orphanRemoval = true, fetch = FetchType.EAGER)
private List<Warehouse> externalWarehouses;
}
This way, I can define LocationsRepository as:
public interface LocationsRepository extends CrudRepository<Location, Long> {
Warehouse findByCityIgnoreCase(String city);
}
Also note that subclass-specific methods can be defined here as long as its return type is explicitly specified (otherwise the method would return ALL Locations, not just the Warehouses).
Finally, in the service layer I can make the relevant methods return any entity just by downcasting the result of the repository call into the appropriate subclass
I can create a repository via defining an interface on the appropriate JPA class A like the following:
public interface ARepository extends CrudRepository<A, Long>
{
}
and I can use that in my Controller (for example) via
#Autowired
private ARepository aRepository;
and just can do things like this:
aRepository.save(..);
aRepository.findAll();
..
No problem so far.
But my problem is that I have ca. 500 JPA classes and need to access each table which means to define 500 Repositories in the style of above.
So does exist an thing to create that either dynamically via some Spring Data "magic" which from my point of view should exist otherwise the above would not be possible. It looks like this is similar to my problem.
Apart from that one more issue related to the above. I can define findBy... methods in the interface and in the background there will be generated a query method for this particular attribute. The question is also if this can be done in a dynamic way related to the previous question, cause I have groups of tables which need supplemental query methods..
There is spring-data-generator which can automatically generate the interfaces for you.
Regarding your 2nd question I don't think you that can be done in a dynamic way. Java is statically compiled and there's no way to add members dynamically. There could be a tool that generates code for those methods but if that tool generates methods for all combinations of columns you will end up with a huge amount of methods.
You can make a base abstract entity for your 500 classes an then create one repo for this class. (I think it's a common practice to have a BaseEntity class with id, version etc. for every entity in the project).
For simple repo methods (like save, findAll etc.) it will work right from the box (note - entities must have the equal id type). For example:
#Entity
#Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
public abstarct class BaseEntity {
#Id
#GeneratedValue(strategy = GenerationType.SEQUENCE)
private Long id;
}
#Entity
public class Entity1 extends BaseEntity {
private String name;
}
#Entity
public class Entity2 extends BaseEntity {
private String name;
}
public interface BaseEntityRepo extends JpaRepository<BaseEntity, Long> {
}
Note that BaseEntity must have #Inheritance(strategy = InheritanceType.TABLE_PER_CLASS) to prevent of using singe table base_entity for every entity. And their ids must not intersect (see #GeneratedValue(strategy = GenerationType.SEQUENCE)).
Usage:
#RunWith(SpringRunner.class)
#SpringBootTest
public class BaseEntityRepoTest {
#Autowired private BaseEntityRepo repo;
#Before
public void setUp() throws Exception {
repo.save(asList(
new Entity1("entity1"),
new Entity2("entity2")
));
}
#Test
public void readingTest() throws Exception {
List<BaseEntity> entities = repo.findAll();
assertThat(entities).hasSize(2);
}
}
Related to your second question you can use this approach:
public interface BaseEntityRepo extends JpaRepository<BaseEntity, Long> {
<T> T findById(Long id, Class<T> type);
}
Usage:
#Test
public void findById() {
final Entity1 entity1 = repo.findById(1L, Entity1.class);
final Entity2 entity2 = repo.findById(2L, Entity2.class);
assertThat(entity1).isNotNull();
assertThat(entity2).isNotNull();
}
But you can build repo query methods only for 'common' properties of inherited entities which are present in the base class. To make this method work you must move the name parameter to the BaseEntity:
<T> List<T> findAllByNameLike(String name, Class<T> type);
I'm on Spring boot 1.4.x branch and Spring Data MongoDB.
I want to extend a Pojo from HashMap to give it the possibility to save new properties dynamically.
I know I can create a Map<String, Object> properties in the Entry class to save inside it my dynamics values but I don't want to have an inner structure. My goal is to have all fields at the root's entry class to serialize it like that:
{
"id":"12334234234",
"dynamicField1": "dynamicValue1",
"dynamicField2": "dynamicValue2"
}
So I created this Entry class:
#Document
public class Entry extends HashMap<String, Object> {
#Id
private String id;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
}
And the repository like this:
public interface EntryRepository extends MongoRepository<Entry, String> {
}
When I launch my app I have this error:
Error creating bean with name 'entryRepository': Invocation of init method failed; nested exception is org.springframework.data.mapping.model.MappingException: Could not lookup mapping metadata for domain class java.util.HashMap!
Any idea?
TL; DR;
Do not use Java collection/map types as a base class for your entities.
Repositories are not the right tool for your requirement.
Use DBObject with MongoTemplate if you need dynamic top-level properties.
Explanation
Spring Data Repositories are repositories in the DDD sense acting as persistence gateway for your well-defined aggregates. They inspect domain classes to derive the appropriate queries. Spring Data excludes collection and map types from entity analysis, and that's why extending your entity from a Map fails.
Repository query methods for dynamic properties are possible, but it's not the primary use case. You would have to use SpEL queries to express your query:
public interface EntryRepository extends MongoRepository<Entry, String> {
#Query("{ ?0 : ?1 }")
Entry findByDynamicField(String field, Object value);
}
This method does not give you any type safety regarding the predicate value and only an ugly alias for a proper, individual query.
Rather use DBObject with MongoTemplate and its query methods directly:
List<DBObject> result = template.find(new Query(Criteria.where("your_dynamic_field")
.is(theQueryValue)), DBObject.class);
DBObject is a Map that gives you full access to properties without enforcing a pre-defined structure. You can create, read, update and delete DBObjects objects via the Template API.
A last thing
You can declare dynamic properties on a nested level using a Map, if your aggregate root declares some static properties:
#Document
public class Data {
#Id
private String id;
private Map<String, Object> details;
}
Here we can achieve using JSONObject
The entity will be like this
#Document
public class Data {
#Id
private String id;
private JSONObject details;
//getters and setters
}
The POJO will be like this
public class DataDTO {
private String id;
private JSONObject details;
//getters and setters
}
In service
Data formData = new Data();
JSONObject details = dataDTO.getDetails();
details.put("dynamicField1", "dynamicValue1");
details.put("dynamicField2", "dynamicValue2");
formData.setDetails(details);
mongoTemplate.save(formData );
i have done as per my business,refer this code and do it yours. Is this helpful?