32

I have a collection called Products in my MongoDB database, which is represented by the interface IProductPrice in my Java code. The following repository declaration causes Spring Date to look to the collection db.collection: Intelliprice.iProductPrice.

I want it to configure it to look in db.collection: Intelliprice.Products using an external configuration rather than putting an @Collection(..) annotation on IProductPrice. Is this possible? How can I do this?

public interface ProductsRepository extends
    MongoRepository<IProductPrice, String> {
}
Oliver Drotbohm
  • 80,157
  • 18
  • 225
  • 211
Danish
  • 3,708
  • 5
  • 29
  • 48

5 Answers5

27

The only way you can currently achieve this is by annotating your domain class with @Document using the collection property to define the name of the collection instances of this class shall be persisted to.

However, there's a JIRA issue open that suggests adding a pluggable naming strategy to configure the ways class, collection and property names are handled in a more global way. Feel free to comment your use case and vote it up.

Oliver Drotbohm
  • 80,157
  • 18
  • 225
  • 211
  • 3
    Thanks, I'm aware of the @Document annotation and probably would end up using that. I basically wanted to externalize the config from the actual class. The JIRA issue you linked to is talking about a naming strategy and still suggests using the annotation for custom names. – Danish Sep 06 '12 at 20:18
  • 1
    The `collection` attribute supports SpEL so that you can invoke arbitrary methods on other Spring beans to calculate the collection name by e.g using `#{#bean.someMethod(T(your.fully.qualified.Type))}` if you have registered a component as `bean` providing a method `someMethod(Class> type)`. – Oliver Drotbohm Dec 30 '14 at 13:20
  • 1
    This doesn't work if you extend your document from parent interface. If this interface is declared in repository signature `ReactiveMongoRepository`, then even hardcoded collection name is ignored - `@Document(collection = "specific_collection_name")`. Collection will be `iproductprice` – Zon Oct 20 '21 at 08:04
  • @Zon In that case you need to use set the collection name at base/parent class level using sepl approach. [This](https://stackoverflow.com/a/54725795/2153380) solution is simple to employ. – CᴴᴀZ Oct 21 '21 at 14:03
  • I have tried SPEL, but then you will have one collection name for all the descendants. If you call some method or pass arguments to a prototype bean - anyway collection name is set only once when the bean is created. I have even tried setting annotation value dynamically - this hadn't helped either. The only option left - is to rewrite Spring Data repositories to MongoTemplate that allows passing collection name with queries. – Zon Oct 21 '21 at 19:59
21

using answer from Oliver Gierke above, working on a project where I need to create multiple collections for one entity, I wanted to use the spring repositories and needed to specify the entity to use before using the repository.

I managed to modify the repository collection name on demand using this system, it using SPeL. You can only work on 1 collection at a time though.

Domain object

@Document(collection = "#{personRepository.getCollectionName()}")
public class Person{}

Default Spring Repository:

public interface PersonRepository 
     extends MongoRepository<Person, String>, PersonRepositoryCustom{
}

Custom Repository Interface:

public interface PersonRepositoryCustom {
    String getCollectionName();

    void setCollectionName(String collectionName);
}

implementation:

public class PersonRepositoryImpl implements PersonRepositoryCustom {

    private static String collectionName = "Person";

    @Override
    public String getCollectionName() {
        return collectionName;
    }

    @Override
    public void setCollectionName(String collectionName) {
        this.collectionName = collectionName;
    }
}

To use it:

@Autowired
PersonRepository personRepository;

public void testRetrievePeopleFrom2SeparateCollectionsWithSpringRepo(){
        List<Person> people = new ArrayList<>();
        personRepository.setCollectionName("collectionA");
        people.addAll(personRepository.findAll());
        personDocumentRepository.setCollectionName("collectionB");
        people.addAll(personRepository.findAll());
        Assert.assertEquals(4, people.size());
}

Otherwise if you need to use configuration variables, you could maybe use something like this? source

@Value("#{systemProperties['pop3.port'] ?: 25}") 
Abhinav Vishak
  • 361
  • 1
  • 5
  • 17
Jeremie
  • 1,267
  • 12
  • 33
  • 1
    haven't tested this, and it's not very clean, but +1 just for being creative :) – alex Mar 10 '16 at 07:44
  • 1
    Seems that you're keeping "context" information within a repository that is potentially auto-wired in various places. My guess that this solution is not thread safe. – thanosa75 May 18 '16 at 12:41
  • @thanosa75 you are right, I was just reusing that solution and thinking that having a repository where you always provide the collection name, would be much much safer: instead of repo.findAll() > repo.findAll("collectionName") . but I don't know how to do that elegantly (rather than recreate a class that reuses a mongo template, and always set the collection name before running the request) – Jeremie May 19 '16 at 17:08
  • @jeremie you could keep a ThreadLocal with this - so no context amongst threads- and wrap the call(s) with a helper that sets the local, something like: Query.for("person", x -> {}) where in the {} you would put the repo calls. The helper would set context, do the call and unset. To see this better in action you could read the Transaction annotation and helpers in Spring. Same concept. – thanosa75 May 22 '16 at 07:39
  • @Jeremie I found the example I was thinking about here: https://doanduyhai.wordpress.com/2011/11/20/spring-transactional-explained/ and you need to check the actual code (look for "invoke" for the juicy bit that explains the aspect and how it works by encapsulating the invocation and setting the transaction). You could have a similar code, that uses a new annotation e.g. @WithCollection("Persons") to encapsulate method declarations and on invocation set the correct collection on the repository. – thanosa75 May 23 '16 at 09:30
  • @thanosa75 thank you for that :)! will have to try it out! – Jeremie Jun 17 '16 at 17:45
  • 1
    This is giving cirular dependency error and when i am removing #{notificationRepository.getCollectionName()} from the Person bean its getting resolved – Rohitesh Mar 14 '17 at 09:46
  • how you can create a new collection in test class ? @Jeremie – Erdem Aydemir Jan 22 '18 at 17:21
  • how would the behavior of the application if the following scenario occur? => We will serve more than one application in practice. When inserting in a service, another serviced search query can be done. My question is, are the same repository of autowired 2 service. What happens if the repository "collection_a" is set when doing an insert, and if the "search" repository is set to "collection_b" before "insert" works? – Erdem Aydemir Jan 22 '18 at 18:17
  • In the project I am working they have used this, but now it is not working anymore. The problem is that 'personRepository' is not defined by the time the expression is evaluated. I am not sure what is the cause of this problem, but I think it was working in a previous version of Spring (not sure if Boot, Data or what) and is not working anymore. – Constantino Cronemberger May 25 '18 at 11:23
  • very late to answer to @forguta, I'm sorry. as I said it would work on 1 collection at a time, so might cause issues if doing parallel queries on 2 collections at a time. would need to set the collection before any query, and even though, if 2 are happening at the same time, might not work. This is just a hack really – Jeremie May 25 '18 at 12:25
  • sorry to hear that @ConstantinoCronemberger how did you find this out, you updated spring version and it stopped working? – Jeremie May 25 '18 at 12:26
  • My problem seems to be in the MongoPersistentEntityIndexCreator which is trying to evaluate the expression before the application context is initialized. – Constantino Cronemberger May 25 '18 at 13:22
  • 1
    Found my problem, it was a project issue because I was creating a custom MongoTemplate based on a brand new MongoMappingContext instead of using the provided MongoMappingContext. – Constantino Cronemberger May 25 '18 at 13:30
9

A little late, but I've found you can set the mongo collection name dynamically in spring-boot accessing the application configuration directly.

@Document(collection = "#{@environment.getProperty('configuration.property.key')}")
public class DomainModel {...}

I suspect you can set any annotation attribute this way.

Jake Stine
  • 91
  • 1
  • 2
3

The only comment I can add is that you have to add @ prefix to the bean name:

collection = "#{@beanName.method()}"

for the bean factory to inject the bean:

@Document(collection = "#{@configRepositoryCustom.getCollectionName()}")
public class Config {

}

I struggled to figure it out..

COMPLETE EXAMPLE:

@Document(collection = "#{@configRepositoryCustom.getCollectionName()}")
public class Config implements Serializable {
 @Id
 private String uuid;
 private String profile;
 private String domain;
 private String label;
 private Map<String, Object> data;
 // get/set
}

 public interface ConfigRepositoryCustom {
   String getCollectionName();
   void setCollectionName(String collectionName);
 }

@Component("configRepositoryCustom")
public class ConfigRepositoryCustomImpl implements ConfigRepositoryCustom {
 private static String collectionName = "config";
 @Override
 public String getCollectionName() {
  return collectionName;
 }
 @Override
 public void setCollectionName(String collectionName) {
 this.collectionName = collectionName;
 }
}

@Repository("configurations")
public interface ConfigurationRepository extends MongoRepository<Config, String>, ConfigRepositoryCustom {
  public Optional<Config> findOneByUuid(String Uuid);
  public Optional<Config> findOneByProfileAndDomain(String profile, String domain);
}

usage in serviceImpl:

@Service
public class ConfigrationServiceImpl implements ConfigrationService {
 @Autowired
 private ConfigRepositoryCustom configRepositoryCustom;

 @Override
 public Config create(Config configuration) {
   configRepositoryCustom.setCollectionName( configuration.getDomain() ); // set the collection name that comes in my example in class member 'domain'
   Config configDB = configurationRepository.save(configuration);
   return configDB;
}
ylev
  • 2,313
  • 1
  • 23
  • 16
  • 1
    I also only manage to get this work with the '@' bean prefix. Not sure about that syntax as it is also not suggested in @Oliver Drotbohm|s mentioned [Jira Issue](https://jira.springsource.org/browse/DATAMONGO-525) where the solution is otherwise also documented. – FrVaBe Mar 26 '20 at 10:17
  • The usage of the '@' prefix for bean references is documented [here](https://docs.spring.io/spring/docs/current/spring-framework-reference/core.html#expressions-bean-references). Thanks Mark for the [hint](https://twitter.com/mp911de/status/1243151041568354306). – FrVaBe Mar 26 '20 at 12:27
  • For some reason, this works for me but ONLY if the first letter of the bean name is lower case. So instead of `#{@ActualBeanName.method()}"`, it works only if I use `#{@actualBeanName.method()}"` – Raphael Oct 28 '21 at 21:11
2

I use static class and method in SpEL;

public class CollectionNameHolder {
    private static final ThreadLocal<String> collectionNameThreadLocal = new ThreadLocal<>();

    public static String get(){
        String collectionName = collectionNameThreadLocal.get();
        if(collectionName == null){
            collectionName = DataCenterApiConstant.APP_WECHAT_DOCTOR_PATIENT_COLLECTION_NAME;
            collectionNameThreadLocal.set(collectionName);
        }
        return collectionName;
    }

    public static void set(String collectionName){
        collectionNameThreadLocal.set(collectionName);
    }

    public static void reset(){
        collectionNameThreadLocal.remove();
    }
}

In Entity class ,@Document(collection = "#{T(com.test.data.CollectionNameHolder).get()}")

And then ,use

CollectionNameHolder.set("testx_"+pageNum) 

in Service , and

CollectionNameHolder.reset();

Hope it helps you.

leimbag
  • 239
  • 2
  • 3