Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to query for Map values with Spring Data JPA?

so my database model goes like this: I have Stores and every Store has a localized name. So I have chosen to represent the localized name as a Map like this:

public class Store {
   private Map<Locale,LocalizedValue> name;
}

as you can see it's a Map of <Locale, LocalizedValue> where the LocalizedValue is a class like this:

@Embeddable
public class LocalizedValue {

   @Column(name = "value")
   private String value;
}

and it all works great. However I get to a problem where I want to query my Spring Data JPA repository and find all the stores with a given english name. So my repository method looks like this:

Store findByName(Map.Entry<Locale, LocalizedValue> name);

but it throws this exception:

 2014-10-07 23:49:55,862 [qtp354231028-165] ERROR: Parameter value [en=Some Value] did not match expected type [com.test.LocalizedValue(n/a)]; nested exception is java.lang.IllegalArgumentException: Parameter value [en=Some Value] did not match expected type [com.test.LocalizedValue (n/a)]
org.springframework.dao.InvalidDataAccessApiUsageException: Parameter value [en=Some Value] did not match expected type [com.test.LocalizedValue (n/a)]; 
nested exception is java.lang.IllegalArgumentException: Parameter value [en=Some Value] did not match expected type [com.test.LocalizedValue (n/a)]
    at org.springframework.orm.jpa.EntityManagerFactoryUtils.convertJpaAccessExceptionIfPossible(EntityManagerFactoryUtils.java:384)
    at org.springframework.orm.jpa.vendor.HibernateJpaDialect.translateExceptionIfPossible(HibernateJpaDialect.java:216)
    at org.springframework.orm.jpa.AbstractEntityManagerFactoryBean.translateExceptionIfPossible(AbstractEntityManagerFactoryBean.java:417)
    at org.springframework.dao.support.ChainedPersistenceExceptionTranslator.translateExceptionIfPossible(ChainedPersistenceExceptionTranslator.java:59)
    at org.springframework.dao.support.DataAccessUtils.translateIfNecessary(DataAccessUtils.java:213)

So then I changed my repository method to be like this:

Store findByName(LocalizedValue name);

but then I got this exception:

Caused by: com.mysql.jdbc.exceptions.jdbc4.MySQLSyntaxErrorException: Unknown column 'name1_.pk' in 'where clause'
    at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
    at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:57)
    at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
    at java.lang.reflect.Constructor.newInstance(Constructor.java:526)

I also tried using Contains in the query method - still no luck.

So my question is: Is there a way to query for the stores with English name 'Some Value'?

like image 507
Petar Tahchiev Avatar asked Oct 07 '14 21:10

Petar Tahchiev


People also ask

Can JPA return results as a map?

If a given JPA GROUP BY query returns only two columns where one is unique, it's very suitable to return the result as a Java Map. For this, you can use either the Java Stream functionality or the Hibernate-specific ResultTransformer .

How do you map native query results to entities?

The easiest way to map a query result to an entity is to provide the entity class as a parameter to the createNativeQuery(String sqlString, Class resultClass) method of the EntityManager and use the default mapping.

How do you return Dtos from native queries with Spring data JPA?

Interface-based DTO projections You first need to define an interface that defines a getter method for each attribute your projection shall contain. At runtime, Spring Data JPA then generates a class that implements that interface. you can then use that interface as the return type of a repository method.

What is findById in JPA?

Its findById method retrieves an entity by its id. The return value is Optional<T> . Optional<T> is a container object which may or may not contain a non-null value. If a value is present, isPresent returns true and get returns the value.


2 Answers

This requires a manually defined query something like this:

interface StoreRepository extends Repository<Store, Long> {

  @Query("select s from Store s join s.map m where ?1 in (VALUE(m))"
  Optional<Store> findByName(String name);
}

It basically tells the persistence provider to expand the map values and check whether the given parameter is in the list of expanded values.

like image 187
Oliver Drotbohm Avatar answered Sep 28 '22 17:09

Oliver Drotbohm


You have not posted your mappings but there seems to me to be a fundamental issue with way that Embeddables are keyed.

This is the only obvious way I can see to map the association:

@Entity
@Table(name = "stores")
public class Store {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Column(name = "id")
    private Long id;

    @ElementCollection
    @CollectionTable(name = "store_names", joinColumns = @JoinColumn(name = "store_id"))
    @MapKeyColumn(name = "locale")
    private Map<Locale, LocalizedName> names;

    public Map<Locale, LocalizedName> getNames() {
        return names;
    }

    public String getName(Locale locale) {
        return names.get(locale).getName();
    }
}


@Embeddable
public class LocalizedName {

    @Column(name = "name")
    private String name;

    @SuppressWarnings("unused")
    private LocalizedName() {

    }

    public LocalizedName(String name) {
        this.name = name;
    }

    public String getName(){
        return name;
    }
}

The following test passes:

@Test
public void testLoadStore() {
    Store store = repository.findOne(1l);
    Assert.assertNotNull(store);
    Assert.assertEquals("EN Name", store.getName(Locale.ENGLISH));
    Assert.assertEquals("DE Name", store.getName(Locale.GERMAN));
}

The issue with this is however that 'Locale' can never be a property of LocalisedName otherwise Hibernate complains of a duplicate column mapping. See the bug report:

https://hibernate.atlassian.net/browse/HHH-5393

So while it is possible to write a query method:

public interface StoreRepository extends JpaRepository {

@Query(value = "from Store s join s.names n where n.name = ?1")
Store findByName(String name);

}

for which the following test passes

@Test
public void testLoadByName() {
    Store store = repository.findByName("EN Name");
    Assert.assertNotNull(store);
    Assert.assertEquals("EN Name", store.getName(Locale.ENGLISH));
    Assert.assertEquals("DE Name", store.getName(Locale.GERMAN));
}

as far as I can see this can never take Locale into account as it is not a property of LocalizedName.

like image 22
Alan Hay Avatar answered Sep 28 '22 17:09

Alan Hay