How To Configure MongoDb Collection Name For a Class in Spring Data

49,734

Solution 1

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.

Solution 2

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}") 

Solution 3

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.

Solution 4

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;
}

Solution 5

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.

Share:
49,734
Danish
Author by

Danish

I design and write software which powers million dollar business, with the power to scale to billions!

Updated on October 14, 2020

Comments

  • Danish
    Danish over 3 years

    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> {
    }
    
  • Danish
    Danish over 11 years
    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.
  • norgence
    norgence over 9 years
    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).
  • alex
    alex about 8 years
    haven't tested this, and it's not very clean, but +1 just for being creative :)
  • thanosa75
    thanosa75 about 8 years
    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.
  • Jeremie
    Jeremie about 8 years
    @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)
  • thanosa75
    thanosa75 almost 8 years
    @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
    thanosa75 almost 8 years
    @Jeremie I found the example I was thinking about here: doanduyhai.wordpress.com/2011/11/20/… 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.
  • Jeremie
    Jeremie almost 8 years
    @thanosa75 thank you for that :)! will have to try it out!
  • Rohitesh
    Rohitesh about 7 years
    This is giving cirular dependency error and when i am removing #{notificationRepository.getCollectionName()} from the Person bean its getting resolved
  • Erdem Aydemir
    Erdem Aydemir over 6 years
    how you can create a new collection in test class ? @Jeremie
  • Erdem Aydemir
    Erdem Aydemir over 6 years
    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?
  • Constantino Cronemberger
    Constantino Cronemberger almost 6 years
    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.
  • Jeremie
    Jeremie almost 6 years
    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
    Jeremie almost 6 years
    sorry to hear that @ConstantinoCronemberger how did you find this out, you updated spring version and it stopped working?
  • Constantino Cronemberger
    Constantino Cronemberger almost 6 years
    My problem seems to be in the MongoPersistentEntityIndexCreator which is trying to evaluate the expression before the application context is initialized.
  • Constantino Cronemberger
    Constantino Cronemberger almost 6 years
    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.
  • FrVaBe
    FrVaBe about 4 years
    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 where the solution is otherwise also documented.
  • FrVaBe
    FrVaBe about 4 years
    The usage of the '@' prefix for bean references is documented here. Thanks Mark for the hint.
  • Zon
    Zon over 2 years
    This doesn't work if you extend your document from parent interface. If this interface is declared in repository signature ReactiveMongoRepository<iProductPrice, String>, then even hardcoded collection name is ignored - @Document(collection = "specific_collection_name"). Collection will be iproductprice
  • CᴴᴀZ
    CᴴᴀZ over 2 years
    @Zon In that case you need to use set the collection name at base/parent class level using sepl approach. This solution is simple to employ.
  • Zon
    Zon over 2 years
    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.
  • iamfrank
    iamfrank over 2 years
    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()}"