How to choose a Spring #Query annotation to suit the DB (e.g. HSQLDB, Postgres)? - spring-data-jpa

Say I have this annotated Java code:
#Transactional
public interface EventRepository extends JpaRepository<Event, Long> {
#Query("from Event where path like ?1% and date(start_time)=CURRENT_DATE")
Stream<Event> findAllChildrenToday(String path);
This works for Postgres. However, when running unit tests I want to use an in-memory database such as HSQLDB. Unfortunately the SQL date(...) command doesn't work on HSQLDB, so I really need something that changes query according to the database like the following in which I've made up an imaginary "db=" tag:
#Query(db="Postgres", "from Event where path like ?1% and date(start_time)=CURRENT_DATE")
#Query(db="HSQLDB", "from Event where path like ?1% and start_time >= CURRENT_DATE and (start_time < CURRENT_DATE + 1)")
Stream<Event> findAllChildrenToday(String path);
How do I do this for real?
I'm not too familiar with Spring so please be fairly detailed in your answers. Thanks.

You can make the following trick with Spring profiles:
#NoRepositoryBean
public interface EventRepo extends JpaRepository<Event, Long> {
Stream<Event> findAllChildrenToday(String path);
}
#Profile("postgres")
public interface PostgresRepo extends EventRepo {
#Override
#Query(/* a postgres specific query */)
Stream<Event> findAllChildrenToday(String path);
}
#Profile("hsqldb")
public interface HsqldbRepo extends EventRepo {
#Override
#Query(/* a hsqldb specific query */)
Stream<Event> findAllChildrenToday(String path);
}
Then in your service (or controller):
#Service
public class EventService {
#Autowired
private EventRepo eventRepo;
//...
}
In the application.properties file you set 'postgres' profile (this profile will be used by default in the production):
spring.profiles.active=postgres
And in your IDE you set 'hsqldb' profile: -Dspring.profiles.active=hsqldb (as JVM option).
Thanks to this the repo injection will depend on the used Spring profile.

Related

Is there any way to force spring not to use/create '_class' field in the mapping?

The thing is on production servers we got mapping for Elasticsearch with dynamic set to strict. Currently, we use a rest level client to communicate with Elastisearch, however, we would like to migrate to spring-data-elasticsearch.
Unfortunately, it seems spring data force to use either _class or #TypeAlias which also interfere with the mapping itself. Is any way to use spring-data without _class or #TypeAlias?
Ok I have found a workaround for it.
Be aware of using it when your elasticsearch model uses inheritance.
To solve this problem create class like this:
public class CustomMappingEsConverter extends MappingElasticsearchConverter {
public CustomMappingEsConverter(MappingContext<? extends ElasticsearchPersistentEntity<?>, ElasticsearchPersistentProperty> mappingContext, GenericConversionService conversionService) {
super(mappingContext, conversionService);
}
#Override
public Document mapObject(#Nullable Object source) {
Document target = Document.create();
if (source != null) {
this.write(source, target);
}
target.remove("_class"); // << workaround to remove those _class field in elasticsearch
return target;
}
}
And register the bean:
#Configuration
public class MappingEsConfiguration {
#Bean
#Primary
public CustomMappingEsConverter CustomMappingElasticsearchConverter(MappingContext<? extends ElasticsearchPersistentEntity<?>, ElasticsearchPersistentProperty> mappingContext,
GenericConversionService genericConversionService) {
return new CustomMappingEsConverter(mappingContext, genericConversionService);
}
}
After this changes I was able to use spring data without additional field _class.
Currently this is not possible. There is an open issue for that.
Edit 25.04.2021:
this feature will be available from the next version (4.3) on.

How to inject spring aop advice for MongoDb call?

I am new to Spring Aop, but I have case to implement AOP advice for a mongo db call(monog db update). I am trying in different way but getting 'Point cut not well formed' error or 'warning no match for this type name: arg string [Xlint:invalidAbsoluteTypeName]'(even if I give absolute name of the argument). Anyone can help on this as how to inject advice for mongo db update call?
#Aspect
#Component
public class DBStatsLoggerAspect {
private static final Logger log = LoggerFactory
.getLogger(DBStatsLoggerAspect.class);
private static final Document reqStatsCmdBson = new Document(
"getLastRequestStatistics", 1);
private DbCallback<Document> requestStatsDbCallback = new DbCallback<Document>() {
#Override
public Document doInDB(MongoDatabase db) throws MongoException,
DataAccessException {
return db.runCommand(reqStatsCmdBson);
}
};
#After("execution( public * com.mongodb.client.MongoCollection.*(..)) && args(org.bson.conversions.Bson.filter,..)")
public void requestStatsLoggerAdvice(JoinPoint joinPoint) {
MongoTemplate mongoTemplate = (MongoTemplate) joinPoint.getTarget();
log.info(mongoTemplate.execute(requestStatsDbCallback).toJson());
}
}
Actual db call method where I need to inject advice:(filter, updatePart all are org.bson.conversions.Bson data type) and here 'collection' is com.mongodb.client.MongoCollection.collection
Document result = collection.findOneAndUpdate(filter, updatePart, new FindOneAndUpdateOptions().upsert(false));
I am not a Spring or MongoDB user, just an AOP expert. But from what I see I am wondering:
You are intercepting execution(public * com.mongodb.client.MongoCollection.*(..)), so joinPoint.getTarget() is a MongoCollection type. Why do you think you can cast it to MongoTemplate? That would only work if your MongoCollection happened to be a MongoTemplate subclass. To me this looks like a bug.
Class MongoCollection is not a Spring component but a third-party class. Spring AOP can only intercept Spring component calls by means of creating dynamic proxies for those components and adding aspect interceptors to said proxies. so no matter how correct or incorrect your pointcut, it should never trigger.
What you can do instead is switch from Spring AOP to full-blown AspectJ. The standard way to do this is to activate AspectJ load-time weaving (LTW).

Problems while connecting to two MongoDBs via Spring

I'm trying to achieve to connect to two different MongoDBs with Spring (1.5.2. --> we included Spring in an internal Framework therefore it is not the latest version yet) and this already works partially but not fully. More precisely I found a strange behavior which I will describe below after showing my setup.
So this is what I done so far:
Project structure
backend
config
domain
customer
internal
repository
customer
internal
service
In configI have my Mongoconfigurations.
I created one base class which extends AbstractMongoConfiguration. This class holds fields for database, host etc. which are filled with the properties from a application.yml. It also holds a couple of methods for creating MongoClient and SimpleMongoDbFactory.
Furthermore there are two custom configuration classes. For each MongoDB one config. Both extend the base class.
Here is how they are coded:
Primary Connection
#Primary
#EntityScan(basePackages = "backend.domain.customer")
#Configuration
#EnableMongoRepositories(
basePackages = {"backend.repository.customer"},
mongoTemplateRef = "customerDataMongoTemplate")
#ConfigurationProperties(prefix = "customer.mongodb")
public class CustomerDataMongoConnection extends BaseMongoConfig{
public static final String TEMPLATE_NAME = "customerDataMongoTemplate";
#Override
#Bean(name = CustomerDataMongoConnection.TEMPLATE_NAME)
public MongoTemplate mongoTemplate() {
MongoClient client = getMongoClient(getAddress(),
getCredentials());
SimpleMongoDbFactory factory = getSimpleMongoDbFactory(client,
getDatabaseName());
return new MongoTemplate(factory);
}
}
The second configuration class looks pretty similar. Here it is:
#EntityScan(basePackages = "backend.domain.internal")
#Configuration
#EnableMongoRepositories(
basePackages = {"backend.repository.internal"}
mongoTemplateRef = InternalDataMongoConnection.TEMPLATE_NAME
)
#ConfigurationProperties(prefix = "internal.mongodb")
public class InternalDataMongoConnection extends BaseMongoConfig{
public static final String TEMPLATE_NAME = "internalDataMongoTemplate";
#Override
#Bean(name = InternalDataMongoConnection.TEMPLATE_NAME)
public MongoTemplate mongoTemplate() {
MongoClient client = getMongoClient(getAddress(), getCredentials());
SimpleMongoDbFactory factory = getSimpleMongoDbFactory(client,
getDatabaseName());
return new MongoTemplate(factory);
}
}
As you can see, I use EnableMongoRepositoriesto define which repository should use which connection.
My repositories are defined just like it is described in the Spring documentation.
However, here is one example which is located in package backend.repository.customer:
public interface ContactHistoryRepository extends MongoRepository<ContactHistoryEntity, String> {
public ContactHistoryEntity findById(String id);
}
The problem is that my backend always only uses the primary connection with this setup. Interestingly, when I remove the beanname for the MongoTemplate (just #Bean) the backend then uses the secondary connection (InternalMongoDataConnection). This is true for all defined repositories.
My question is, how can I achieve that my backend really take care of both connections? Probably I missed to set another parameter/configuration?
Since this is a pretty extensive post I apologise if I forgot something to mention. Please ask for missing information in the comments.
I found the answer.
In my package structure there was a empty configuration class (of my colleague) with the annotation #Configurationand #EnableMongoRepositories. This triggered the automatic wiring process of Stpring Data and therefore led to the problems I reported above.
I simply deleted the class and now it works as it should!

Spring Data Rest with Spring Security - find all by current user

Is it possible to use Spring Data Rest and Spring Security to return current user related entities, using the findAll() method without specifying this user in the GET query parameter?
My only solution is to pass user as a parameter, but maybe it's another option to get him from SpringSecurityContext
public interface InvoiceRepository extends CrudRepository<Invoice, Long> {
#RestResource
#PreAuthorize("hasRole('ROLE_ADMIN') or user?.username == authentication.name")
List<Invoice> findAllByUser(#Param("user") User user);
You can use SpEL EvaluationContext extension that makes security properties and expressions available in SpEL expressions in the #Query annotations. This allows you to get only those business objects that relate to the current user:
interface SecureBusinessObjectRepository extends Repository<BusinessObject, Long> {
#Query("select o from BusinessObject o where o.owner.emailAddress like ?#{hasRole('ROLE_ADMIN') ? '%' : principal.emailAddress}")
List<BusinessObject> findBusinessObjectsForCurrentUser();
}
More details are here.

How to shorten names of query methods in Spring Data JPA Repositories?

Consider a Spring Data Jpa Repository:
public interface UserRepository extends JpaRepository<User, Long> {
User findOneByDeletedIsFalseAndActivationKey(String activationKey);
List<User> findAllByDeletedIsFalseAndActivatedIsFalseAndCreatedDateBefore(DateTime dateTime);
User findOneByDeletedIsFalseAndLogin(String login);
User findOneByDeletedIsFalseAndEmail(String email);
}
Notice each method has "DeletedIsFalse" in it. Is there a simple way to make method names shorter? Like i.e.:
#FullMethodName("findOneByDeletedIsFalseAndEmail")
User findOneByEmail(String email);
Use default Java 8 feature for wrapping, just like so:
interface UserInterface extends JpaRepository<User, Long> {
// use findOneByEmail instead
User findOneByDeletedIsFalseAndEmail(String email);
default User findOneByEmail(String email) {
return findOneByDeletedIsFalseAndEmail(email);
}
}
See an example.
With Kotlin, you can use extension functions, for example:
interface UserRepository : JpaRepository<User, Long> {
// use findOneByEmail instead
fun findOneByDeletedIsFalseAndEmail(email: String): User
}
fun UserRepository.findOneByEmail(email: String) =
findOneByDeletedIsFalseAndEmail(email)
Now you can use Java 8 default interface methods as #Maksim Kostromin described. But there is no such a feature in Spring.
-- Old answer
There is no such a way. You can specify any name for a method and add an annotation #Query with parameter value which holds desired query to database like this:
#Query(value="select u from User u where u.deleted=false and u.email=:email")
User findOneByEmail(#Param("email")String email);
or, with native sql query:
#Query(value="SELECT * FROM users WHERE deleted=false AND email=?1", nativeQuery=true)
User findOneByEmail(String email);
You can also use names that follow the naming convention for queries since #Query annotation will take precedence over query from method name.
#Query docs
Upd:
from Spring docs:
Although getting a query derived from the method name is quite convenient, one might face the situation in which ... the method name would get unnecessarily ugly. So you can either use JPA named queries through a naming convention ... or rather annotate your query method with #Query.