7

I started to work with Spring Data Elasticsearch on Spring Boot 1.3.1 and I want to use the same Entity that use in my database, and it has compound key.

Entity class:

@IdClass(PassengerPk.class)
@Table(name = "passenger")
@Document(indexName="passenger")
public class Passenger implements Serializable {

    @Id
    @ManyToOne
    @JoinColumn(columnDefinition="long", name="user_id", referencedColumnName="id")
    private User user;

    @Id
    @ManyToOne
    @JoinColumn(columnDefinition="long", name="scheduler_id", referencedColumnName="id")
    private Scheduler scheduler;

    @Column(name = "is_active")
    private Boolean isActive;

    ...
}

Key class:

public class PassengerPk implements Serializable {

    private Long user;
    private Long scheduler;

    public PassengerPk() {
    }

    public PassengerPk(Long user, Long scheduler) {
        this.user = user;
        this.scheduler = scheduler;
    }
    ...
}

JPA Elasticsearch repository:

public interface PassengerSearchRepository extends ElasticsearchRepository<Passenger, PassengerPk> {

}

Database: database relationships

If I try to compile this code, I get this error.

Caused by: java.lang.IllegalArgumentException: Unsuppored ID type class com.dualion.test.domain.PassengerPk
    at org.springframework.data.elasticsearch.repository.support.ElasticsearchRepositoryFactory.getRepositoryBaseClass(ElasticsearchRepositoryFactory.java:79) ~[spring-data-elasticsearch-1.3.1.RELEASE.jar:na]
    at org.springframework.data.repository.core.support.RepositoryFactorySupport.getRepositoryInformation(RepositoryFactorySupport.java:238) ~[spring-data-commons-1.11.1.RELEASE.jar:na]
    at org.springframework.data.repository.core.support.RepositoryFactorySupport.getRepository(RepositoryFactorySupport.java:181) ~[spring-data-commons-1.11.1.RELEASE.jar:na]
    at org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport.initAndReturn(RepositoryFactoryBeanSupport.java:251) ~[spring-data-commons-1.11.1.RELEASE.jar:na]
    at org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport.afterPropertiesSet(RepositoryFactoryBeanSupport.java:237) ~[spring-data-commons-1.11.1.RELEASE.jar:na]
    at org.springframework.data.elasticsearch.repository.support.ElasticsearchRepositoryFactoryBean.afterPropertiesSet(ElasticsearchRepositoryFactoryBean.java:55) ~[spring-data-elasticsearch-1.3.1.RELEASE.jar:na]
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.invokeInitMethods(AbstractAutowireCapableBeanFactory.java:1637) ~[spring-beans-4.2.3.RELEASE.jar:4.2.3.RELEASE]
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1574) ~[spring-beans-4.2.3.RELEASE.jar:4.2.3.RELEASE]
    ... 71 common frames omitted

How I can modify my code?

Thanks

kn4ls
  • 71
  • 2
  • 6
  • anybody can give me a solution? – kn4ls Jan 09 '16 at 16:19
  • 1
    How do you inherit `@IdClass` dependency? I think that it is a class brought by some JPA dependency, ES with JPA is not so trivial to configure, I don't know how. So of course you cannot use it for ES. By the way would be very useful for me to know how to achieve your goal. – andPat Feb 18 '16 at 13:52
  • Finally use HibernateSearch and not ES, beacause implementation it's more easy. – kn4ls Oct 15 '16 at 14:07

1 Answers1

1

I had read related answers elsewhere and had concluded this was not possible; however, my stubbornness got the best of me and I figured out a solution.

TLDR; Force spring to use a new repository that takes the hashCode of your composite id and uses its String value as its id.

Steps...

Create a new Repository that can handle composite ids:

public class HashKeyedRepository<T, ID extends Serializable> extends AbstractElasticsearchRepository<T, ID> {

    public HashKeyedRepository() {
        super();
    }

    public HashKeyedRepository(ElasticsearchEntityInformation<T, ID> metadata,
                                 ElasticsearchOperations elasticsearchOperations) {
        super(metadata, elasticsearchOperations);
    }

    public HashKeyedRepository(ElasticsearchOperations elasticsearchOperations) {
        super(elasticsearchOperations);
    }

    @Override
    protected String stringIdRepresentation(ID id) {
        return String.valueOf(id.hashCode());
    }
}

Note that this assumes that you have implemented .hashCode on your composite id class correctly to work.

Next you have to create a new RepositoryFactoryBean that will return this new Repository:

public class CustomElasticsearchRepositoryFactoryBean<T extends Repository<S, ID>, S, ID extends Serializable> extends ElasticsearchRepositoryFactoryBean<T, S, ID> {

    private ElasticsearchOperations operations;

    public CustomElasticsearchRepositoryFactoryBean(Class<? extends T> repositoryInterface) {
        super(repositoryInterface);
    }

    public void setElasticsearchOperations(ElasticsearchOperations operations) {
        super.setElasticsearchOperations(operations);
        Assert.notNull(operations);     
        this.operations = operations;
    }

    @Override
    protected RepositoryFactorySupport createRepositoryFactory() {
        return new ElasticsearchRepositoryFactory(operations) {
            @Override
            protected Class<?> getRepositoryBaseClass(RepositoryMetadata metadata) {
                if (!Integer.class.isAssignableFrom(metadata.getIdType()) && !Long.class.isAssignableFrom(metadata.getIdType()) && !Double.class.isAssignableFrom(metadata.getIdType()) && metadata.getIdType() != String.class && metadata.getIdType() != UUID.class) {
                    return HashKeyedRepository.class;
                }
                return super.getRepositoryBaseClass(metadata);
            }
        };
    }
}

Finally, when enabling your repositories, specify your new RepositoryFactoryBean class:

@EnableElasticsearchRepositories(basePackages = "xxx.xxx.repository.search", repositoryFactoryBeanClass = CustomElasticsearchRepositoryFactoryBean.class)

This implementation falls back to the default if any of the supported IDs as of this writing are used instead (i.e. String, UUID, Number). I don't know if its a GREAT solution given that there are possibilities of conflict with .hashCode but its working for me right now.

PS I'm using lomboks @Data to auto generate .hashCode for me.

PPS Another solution I've seen others (non java) mention is to base64 encode a serialized version of the id (i.e. JSON). I think this would guarantee no conflicts but you'd have to be sure to strip any excess characters (i.e. whitespace) and guarantee order of properties for it to be effective.

Andrew Wynham
  • 2,310
  • 21
  • 25
  • hashing is not unique, you will end up with duplicate ids – Konstantin Kulagin Apr 14 '20 at 13:07
  • I made note of that. But if your composite ID consists of let’s say two unique integers and you use a good hashcode implementation utilizing prime numbers correctly, the chances of conflict should be minimal. Of course the larger your data set the greater the chance but even then you can decide if the chance of conflict effects your end solution (I.e. implementing global search on top of a DB, can you tolerate missing 1/x results if their composite IDs conflict?). – Andrew Wynham Apr 22 '20 at 19:08