Spring Data Solr Tutorial: Adding Custom Methods to All Repositories

If we are using Spring Data Solr in a real life software project, the odds are that sooner or later we will face a requirement which states that our application must be able to communicate with both a local Solr server and a SolrCloud. At the moment, fulfilling this requirement means that we have to add custom methods to all Spring Data Solr repositories.

This blog post describes how that is done.

As an example, we will modify the example application of the previous part of my Spring Data Solr tutorial. During this blog post we will change the custom repository implementation of that application in a such way that all its methods are added to all repositories.

This is of course a bit naive example because the custom interface and its implementation are both tied to the TodoDocument class.

We can add custom methods to all repositories by following these steps:

  1. Get the required dependencies with Maven
  2. Create an interface which declares the custom methods.
  3. Implement the created interface.
  4. Create a custom repository factory bean.
  5. Configure Spring Data Solr to use the custom repository factory bean.

Enough with chit chat. Let's get started.

Getting the Required Dependencies with Maven

The example application of this blog post uses a build snapshot of Spring Data Solr because it provides a better support for implementing custom repository factory beans. We can get the required dependencies by making the following changes to our POM file:

  1. Add the Spring snapshot repository to the repositories section of the pom.xml file.
  2. Change the version of the Spring Data Solr dependency.

These steps are described with more details in the following subsections.

Using Spring Snapshot Repository

We can use the Spring snapshot Maven repository by adding the following repository configuration to our POM file:

<repositories>
    <repository>
        <id>spring-snapshots</id>
        <name>Spring Snapshot Maven Repository</name>
        <url>http://repo.springsource.org/libs-snapshot</url>
    </repository>
</repositories>

Updating Spring Data Solr Version

We can use the build snapshot of Spring Data Solr by adding the following dependency declaration to the pom.xml file.

<dependency>
    <groupId>org.springframework.data</groupId>
    <artifactId>spring-data-solr</artifactId>
    <version>1.0.0.BUILD-SNAPSHOT</version>
</dependency>

Creating the Custom Repository Interface

We can create a custom interface for our repositories by following these steps:

  1. Create an interface called CustomBaseRepository which has two type parameters: The type of document (T) and the id of the document (ID).
  2. Ensure that the CustomBaseRepository interface extends the SolrCrudRepository interface.
  3. Annotate the interface with the @NoRepositoryBean annotation. This ensures that Spring Data Solr will not try to create an implementation for our interface.
  4. Add the method declarations of the count() and update() methods to the CustomBaseRepository interface.

The source code of the CustomBaseRepository interface looks as follows:

import org.springframework.data.repository.NoRepositoryBean;
import org.springframework.data.solr.repository.SolrCrudRepository;

import java.io.Serializable;

@NoRepositoryBean
public interface CustomBaseRepository<T, ID extends Serializable> extends SolrCrudRepository<T, ID> {

    public long count(String searchTerm);

    public void update(Todo todoEntry);
}

Our next step is to implement the created interface. Let's find out how this is done.

Implementing the Custom Repository Interface

We can implement the custom repository by following these steps:

  1. Create a class called CustomBaseRepositoryImpl. This class has two type parameters: the type of the document (T) and the type of the document’s id (ID).
  2. Ensure that the created class extends the SimpleSolrRepository class and implements the CustomBaseRepository interface.
  3. Create a constructor which takes a SolrOperations object and the type of the document class as constructor arguments. The implementation of this constructor simply calls the constructor of the superclass.
  4. Implement the update() method. Because the implementation of this method has been described in this blog post, I will not go into details here.
  5. Implement the count() method. Again, I will not go into details here because the implementation of this method has been described earlier.

The source code of the CustomBaseRepositoryImpl class looks as follows:

import org.springframework.data.solr.core.SolrOperations;
import org.springframework.data.solr.core.query.Criteria;
import org.springframework.data.solr.core.query.PartialUpdate;
import org.springframework.data.solr.core.query.SimpleQuery;
import org.springframework.data.solr.repository.support.SimpleSolrRepository;

import java.io.Serializable;

public class CustomBaseRepositoryImpl<T, ID extends Serializable> extends SimpleSolrRepository<T, ID> implements CustomBaseRepository<T, ID> {

    public CustomBaseRepositoryImpl(SolrOperations solrOperations, Class<T> entityClass) {
        super(solrOperations, entityClass);
    }

    @Override
    public long count(String searchTerm) {
        String[] words = searchTerm.split(" ");
        Criteria conditions = createSearchConditions(words);
        SimpleQuery countQuery = new SimpleQuery(conditions);

        return getSolrOperations().count(countQuery);
    }

    private Criteria createSearchConditions(String[] words) {
        Criteria conditions = null;

        for (String word: words) {
            if (conditions == null) {
                conditions = new Criteria("title").contains(word)
                        .or(new Criteria("description").contains(word));
            }
            else {
                conditions = conditions.or(new Criteria("title").contains(word))
                        .or(new Criteria("description").contains(word));
            }
        }

        return conditions;
    }

    @Override
    public void update(Todo todoEntry) {
        PartialUpdate update = new PartialUpdate("id", todoEntry.getId().toString());

        update.add("description", todoEntry.getDescription());
        update.add("title", todoEntry.getTitle());

        getSolrOperations().saveBean(update);
        getSolrOperations().commit();
    }
}

Let's move and find out how we can create a custom repository factory bean.

Creating the Custom Repository Factory Bean

The repository factory bean is a component which is responsible of creating implementations for the repository interfaces. Because we want to use the CustomBaseRepositoryImpl class as an implementation of our Spring Data Solr repositories, we have to create a custom repository factory bean.

We can create a new repository factory bean by following these steps:

  1. Create a class called CustomSolrRepositoryFactoryBean which extends the SolrRepositoryFactoryBean class.
  2. Add a private CustomSolrRepositoryFactory class to the CustomSolrRepositoryFactory bean class. This class extends the SolrRepositoryFactory class and it has two type parameters: the type of document (T) and the type of the document’s id (ID).
  3. Override the doCreateRepositoryFactory() method of the SolrRepositoryFactoryBean class. The implementation of this method returns a new CustomSolrRepositoryFactory object.

Let's take a closer look at the implementation of the CustomSolrRepositoryFactory class. We can implement it by following these steps:

  1. Add a SolrOperations field to the CustomSolrRepositoryFactory class.
  2. Add a constructor to the CustomSolrRepositoryFactory class. This class takes the used SolrOperations object as a constructor argument. Its implementation will simply call the constructor of the superclass and set the received SolrOperations object to the field which we created in the step one.
  3. Override the getTargetRepository() method of the SolrRepositoryFactory class and return a new CustomBaseRepositoryImpl object.
  4. Override the getRepositoryBaseClass() method of the SolrRepositoryFactory class and return the type of our custom interface.

That's it. The source code of our custom repository factory bean looks as follows:

import org.springframework.data.repository.core.RepositoryMetadata;
import org.springframework.data.repository.core.support.RepositoryFactorySupport;
import org.springframework.data.solr.core.SolrOperations;
import org.springframework.data.solr.repository.support.SolrRepositoryFactory;
import org.springframework.data.solr.repository.support.SolrRepositoryFactoryBean;

import java.io.Serializable;

public class CustomSolrRepositoryFactoryBean extends SolrRepositoryFactoryBean {

    @Override
    protected RepositoryFactorySupport doCreateRepositoryFactory() {
        return new CustomSolrRepositoryFactory(getSolrOperations());
    }

    private static class CustomSolrRepositoryFactory<T, ID extends Serializable> extends SolrRepositoryFactory {

        private final SolrOperations solrOperations;

        public CustomSolrRepositoryFactory(SolrOperations solrOperations) {
            super(solrOperations);
            this.solrOperations = solrOperations;
        }
		
        @Override
        protected Object getTargetRepository(RepositoryMetadata metadata) {
            return new CustomBaseRepositoryImpl<T, ID>(solrOperations, (Class<T>) metadata.getDomainType());
        }

		@Override
        protected Class<?> getRepositoryBaseClass(RepositoryMetadata metadata) {
            return CustomBaseRepository.class;
        }
    }
}

Our next is to configure Spring Data Solr to use the repository factory bean which we just created. Let's get started.

Configuring Spring Data Solr

Our last step is to configure Spring Data Solr to use the new repository factory bean which we created in the previous step. We can do this by using either a Java configuration class or an XML configuration file. Both of these options are described in the following subsections.

Note: The different configuration files presented in the following subsections are simplified for the sake of clarity. In reality, our example application has different configuration for development and production environment.

Java Configuration

If we are using Java configuration, we can configure Spring Data Solr to use a custom repository factory bean by using the repositoryFactoryBeanClass attribute of the @EnableJpaRepositories annotation. The source code of configuration class looks as follows:

import org.springframework.context.annotation.Configuration;
import org.springframework.data.solr.repository.config.EnableSolrRepositories;

@Configuration
@EnableSolrRepositories(
        basePackages = "net.petrikainulainen.spring.datasolr.todo.repository.solr",
        repositoryFactoryBeanClass = CustomSolrRepositoryFactoryBean.class
)
public class SolrContext {

    //Configuration is omitted.
}

XML Configuration

When we are using XML configuration, we can configure Spring Data Solr to use a custom repository factory bean by using the factory-class attribute of the repositories namespace element. The XML configuration file of our application context looks as follows:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:solr="http://www.springframework.org/schema/data/solr"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.2.xsd http://www.springframework.org/schema/data/solr http://www.springframework.org/schema/data/solr/spring-solr.xsd">

    <!-- Enable Solr repositories and configure repository base package -->
    <solr:repositories base-package="net.petrikainulainen.spring.datasolr.todo.repository.solr" 
                       factory-class="net.petrikainulainen.spring.datasolr.todo.repository.solr.CustomSolrRepositoryFactoryBean"/>

    <!-- The configuration is omitted. -->
</Beans>

Summary

We have now created two custom methods which are added to all repositories to our example application. Of course, like we learned earlier, this example does not make any sense because our custom repository interface and its implementation is tied to the TodoDocument class.

This tutorial has taught us two things:

  • We can use the @NoRepositoryBean annotation to signal Spring Data Solr that it should not create an implementation for the interface which is annotated with the @NoRepositoryBean annotation.
  • We can configure a custom repository factory bean by using either the repositoryFactoryBeanClass attribute of the @EnableSolrRepositories annotation or the factory-class attribute of the repositories namespace element.

As always, the example application of this blog post is available at Github.

If you want to learn how to use Spring Data Solr, you should read my Spring Data Solr tutorial.
13 comments… add one
  • Rupanjan Sep 26, 2013 @ 23:02

    Petri,

    Sorry to trouble you again..referred http://stackoverflow.com/questions/16859181/spring-data-solr-multiple-cores-and-repository

    RepositoryMembershipIndexService.java file as follows
    ==================================
    where I tried to create 'repository' manually instead of autowiring

    package net.pegonwheels.spring.datasolr.domain.service;

    @Service
    public class RepositoryMembershipIndexService implements MembershipIndexService {

    private static final Logger LOGGER = LoggerFactory
    .getLogger(RepositoryMembershipIndexService.class);

    @Autowired
    private SolrTemplate solrMembershipTemplate;

    private MembershipDocumentRepository repository = new SolrRepositoryFactory(
    this.solrMembershipTemplate)
    .getRepository(MembershipDocumentRepository.class);

    @Transactional
    @Override
    public void addToIndex(Membership membershipEntry) {
    LOGGER.debug("Saving a Membership entry with information: {}",
    membershipEntry);
    MembershipDocument document = MembershipDocument.getBuilder(
    ...
    LOGGER.debug("Saving document with information: {}", document);

    repository.save(document);
    }
    }

    applicationContext-solr.xml file as follows
    =============================

    At server startup, I am getting the following exception.. I guess it's expecting a default constructor somewhere,

    org.springframework.beans.BeanInstantiationException: Could not instantiate bean class [net.pegonwheels.spring.datasolr.domain.service.RepositoryMembershipIndexService]: Constructor threw exception; nested exception is java.lang.IllegalArgumentException: [Assertion failed] - this argument is required; it must not be null
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory
    .instantiateBean(AbstractAutowireCapableBeanFactory.java:1011)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory
    .createBeanInstance(AbstractAutowireCapableBeanFactory.java:957)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory
    .doCreateBean(AbstractAutowireCapableBeanFactory.java:490)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(

    • Petri Sep 26, 2013 @ 23:49

      Hi,

      Don't worry about bothering me. It is nice to help! Anyway, the problem is that when you create the repository, the SolrTemplate field has not been injected yet, and a null value is passed as a constructor parameter.

      I would use constructor injection and create the repository inside the constructor. The modified code looks as follows:

      
      @Service
      public class RepositoryMembershipIndexService implements MembershipIndexService {
      
      	private SolrTemplate solrTemplate;
      	
      	private MembershipDocumentRepository repository;
      
      	@Autowired
      	public RepositoryMembershipIndexService(SolrTemplate solrTemplate) {
      		//If you don't need SolrTemplate, you might want to remove the field.
      		this.solrTemplate = solrTemplate;
      		repository = new SolrRepositoryFactory(this.solrTemplate)
      				.getRepository(MembershipDocumentRepository.class);
      	}
      }
      
      
  • WS Oct 15, 2013 @ 4:48

    Hi Petri, I've been trying to figure out how to search on words and phrases in the same query. Such as, q=cat "brown dog" mouse

    I'd like to do: .contains("cat"), .contains("brown dog") and .contains("mouse") in the same Criteria.

    I've seen that spring-data-solr has an assert on spaces in contains(). Do you have any suggestions?

    Thanks, WS

    • Petri Oct 20, 2013 @ 22:27

      Do you want to limit your search to specific fields of the Solr document or do you want to just a provide a value for the q request parameter?

  • WS Oct 21, 2013 @ 22:17

    I believe Christoph answered my question [http://stackoverflow.com/questions/19381334/how-to-search-on-phrases-in-spring-data-solr-1-0] Using Criteria.is() seems to work fine for phrase searches. This was restricting the request parameter to a particular field. This is what I am using now. `new Criteria("source").is(word);`

    I was just about to post another question when I read your reply. This one seems tricky if possible.

    I need a nested query constructed like the following,

    q=source:word AND (visibility:visible^1000 OR visibility:archived)

    Was hoping something like the following would work but if I chain Criterias inside of .and() or .or() all but one get dropped.

    Criteria c = new Criteria("source").is("word")
    .and(new Criteria("visibility").is("visible").boost(1000)
    .or(new Criteria("visibility").is("archived")));

    Do you think this is possible? Thanks /w

    • Petri Oct 22, 2013 @ 22:46

      I also ran into some problems when I tried to chain criterias in the similar manner. I ended up by following these steps:

      1. I added custom method to my Spring Data Solr repository.
      2. I created the Solr query string manually and fetched the query results by calling the queryForPage method of the SolrTemplate class

      You should be able to create and execute the query by using this code:

      
      //Create criteria and query
      Criteria criteria = new SimpleStringCriteria("");
      SimpleQuery query = new SimpleQuery(criteria);
      
      //Execute query
      Page<Document> results = solrTemplate.queryForPage(criteria, Document.class);
      
      

      I hope that this answered to your question.

  • WS Oct 24, 2013 @ 23:55

    Hmm, I hadn't noticed SimpleStringCriteria(); you never cease to amaze me - that worked like a charm. :) Thanks again.

  • Dean Aug 18, 2015 @ 20:55

    I keep getting [Assertion Failed] - this argument is required; it must not be null

    When I remove all fields from the Book object (except for Id) everything works. As soon as I add in the 'name' field, it breaks it producing the above error.

    One difference is that I am using Solr 5 in my tests...

    • Petri Aug 18, 2015 @ 22:18

      When do you get that error?

  • Conquer Jul 26, 2017 @ 5:01

    Sorry to bother you, But I got a problem.
    When I added Custom Methods to All Repositories, the added methods works fine, but when I use
    the methods belong to each Repositories, like findByQ3a as below:
    public interface TestViewRepository extends CustomBaseRepository {
    List findByQ3a(String string, PageRequest pageRequest);
    }
    as q3a is a field of TestView, I got the error:
    Expected mime type application/octet-stream but got text/html.
    Can you give me some advice?
    Thanks in advance.

    • Petri Aug 1, 2017 @ 0:21

      Hi,

      You are not bothering me at all. The problem is that you provided so little information that i cannot say what is wrong. I assume that the problem is somehow related to the communication between your application and the used Solr server. Can I see the full stack trace?

  • Anonymous Jan 21, 2022 @ 13:43

    Hi Petri,

    I'm getting this , could you help?

    CustomRepositoryImpl is not abstract and does not override abstract method deleteAllById(java.lang.Iterable) in org.springframework.data.repository.CrudRepository

    • Petri Jan 22, 2022 @ 21:16

      Hi,

      The error message suggests that your implementation doesn't implement the deleteAllById() method of the CrudRepository interface. Does your version of the CustomRepositoryImpl extend the "project specific" implementation of that interface (like the SimpleSolrRepository class)?

Leave a Reply