Search code examples
javajpaopenjpa

Need to get MORE than one result from the entity manager


I've been trying to figure this one out for the last little while. In complete contrast to all those questions asking how to only get one result from a JPA query, I need more than one result - I need all of them. I end up calling the entity manager's find() method, but it doesn't get me all of the nested results.

'Parent' class

@Entity
@Table(name = "LANGUAGE")  
public class LanguageData extends PersistedAuditedData<Long> {

@Id
@Column
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

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

@OneToMany(fetch = FetchType.LAZY, mappedBy = "languageData")
@MapKeyColumn(name = "LANGUAGE_ID")
private Map<Long, LanguageDataLocalization> localizations;

    @OneToMany(fetch = FetchType.LAZY, mappedBy = "localizationLanguage")
@MapKeyColumn(name = "LANGUAGE_LOCALIZATION_ID")
private Map<Long, LanguageDataLocalization> languagesLocalized;

/**
 * Private no-arg constructor for reflection-based 
     * construction inside JPA providers.
 */
@SuppressWarnings("unused")
private LanguageData() {
    // Fields initialized by JPA.
}

    // Plus getters for the fields.

}

'Child' class

@Entity
@Table(name = "LANGUAGE_LOCALIZATION")
public class LanguageDataLocalization extends PersistedAuditedData<LanguageLocalizationKey>{

@EmbeddedId
private LanguageLocalizationKey id;

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

@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "LANGUAGE_ID", insertable = false, updatable = false)
private LanguageData languageData;

@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "LANGUAGE_LOCALIZATION_ID", insertable = false, updatable = false)
private LanguageData localizationLanguage;

/**
 * Private no-arg constructor for reflection-based JPA provider instantiation.
 */
@SuppressWarnings("unused")
private LanguageDataLocalization() {
    // Fields initialized by JPA.
}

    // Plus getters for fields.
}

Key for 'Child' class.

@Embeddable
public class LanguageLocalizationKey {

@Column(name = "LANGUAGE_ID")
private Long languageId;

@Column(name = "LANGUAGE_LOCALIZATION_ID")
private Long languageLocalizationId;

/**
 * No-arg constructor, for use by JPA provider implementation.
 * <h1>DO NOT USE!</h1>
 * <p>Ideally, this should be <code>private</code>, 
     * however OpenJPA doesn't appear to be allowing that at the moment   
 * (unsure of cause).  This constructor does not initialize any data.</p>
 */
@SuppressWarnings("unused")
public LanguageLocalizationKey() {
    // Field initialization performed by JPA provider.
}

    // Plus getters
}

And used like so:

@Transactional(readOnly = true, propagation = Propagation.SUPPORTS)
private Collection<LanguageDataLocalization> getLocalizationsById(final Long id, final Collection<String> localeCodes) {
    try {
        if (localeCodes == null || localeCodes.isEmpty()) {
            final LanguageData data = entityManager.find(LanguageData.class, id);
            if (data == null) {
                return Collections.emptyList();
            } else {
                return data.getlocalizations().values();
            }
        } else {
            List<LanguageDataLocalization> results = entityManager.createNamedQuery("FIND_LANGUAGE_BY_ID", LanguageDataLocalization.class)
                                                                    .setParameter("iso6391Alpha2Codes", localeCodes)
                                                                    .setParameter("languageId", id)
                                                                    .getResultList();
            return results;
        }
    } catch (NoResultException e) {
        // TODO: add logging
        return Collections.emptyList();
    }
}

With named-queries.xml defined like so:

<?xml version="1.0" encoding="UTF-8"?>
<entity-mappings version="1.0"
xmlns="http://java.sun.com/xml/ns/persistence/orm" 
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/persistence/orm
                    http://java.sun.com/xml/ns/persistence/orm_1_0.xsd ">

<named-query name="FIND_LANGUAGE_BY_ID">
    <query>
        SELECT localized 
        FROM LanguageDataLocalization localized
        JOIN localized.localizationLanguage local
        WHERE localized.id.languageId = :languageId
        AND local.iso6391Alpha2Code IN :iso6391Alpha2Codes
    </query>
</named-query>
</entity-mappings>

LANGUAGE data (Derby in-memory database, pretty sure that's not important):

ID,  ISO6391ALPHA2CODE
123, en
137, fr

LANGUAGE_LOCALIZATION data

LANGUAGE_ID, LANGUAGE_LOCALIZATION_ID, REFERENCE_NAME
123,         123,                      English
123,         137,                      anglais

The actual problem is that when queried without any locale data (localeCodes is null or empty), data.getLocalizations().values() after the entityManager.find() returns a list of only one of the localized language-name data (the french one, but I'm assuming that's SQL 'random'). I am able to get both localizations if I explicitly query for them (either singly or together), so I know the data's there, and that the JPQL query works.

What can I do to make it return both (all, when I have localization data for more than 2 languages) localizations when not queried for a specific one?


The actual query seems to be this (some result columns removed):

SELECT t0.LANGUAGE_ID, t0.LANGUAGE_LOCALIZATION_ID, 
       t1.ISO6391ALPHA2CODE, 
       t0.REFERENCE_NAME 
FROM LANGUAGE_LOCALIZATION t0 
LEFT OUTER JOIN LANGUAGE t1 
ON t0.LANGUAGE_LOCALIZATION_ID = t1.id 
WHERE t0.LANGUAGE_ID = ? [params=?]

Which doesn't appear to be restricting the row count in any fashion. (to.LANGUAGE_ID matches more than one row). Especially because, over the same dataset, the following gets both result rows (some result columns removed again):

SELECT t0.LANGUAGE_ID, t0.LANGUAGE_LOCALIZATION_ID, 
       t2.id, t2.ISO6391ALPHA2CODE, 
       t3.id, t3.ISO6391ALPHA2CODE, 
       t0.REFERENCE_NAME 
FROM LANGUAGE_LOCALIZATION t0 
INNER JOIN LANGUAGE t1 
ON t0.LANGUAGE_LOCALIZATION_ID = t1.id 
LEFT OUTER JOIN LANGUAGE t2 
ON t0.LANGUAGE_ID = t2.id 
LEFT OUTER JOIN LANGUAGE t3 
ON t0.LANGUAGE_LOCALIZATION_ID = t3.id 
WHERE (t0.LANGUAGE_ID = ? AND t1.ISO6391ALPHA2CODE IN (?, ?)) [params=?, ?, ?]

(Which brings up the question of why it has both t1 and t3, but this is something of a lesser concern at the moment.)


Solution

  • I finally figured out what was wrong -
    I'd mapped the one-to-many maps to key off the wrong columns. The definition of LanguageData needed to be changed to:

    @Entity
    @Table(name = "LANGUAGE")
    public class LanguageData extends PersistedAuditedData<Long> {
    
    @Id
    @Column
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @Column(name = "ISO6391ALPHA2CODE")
    private String iso6391Alpha2Code;
    
    @OneToMany(fetch = FetchType.LAZY, mappedBy = "languageData")
    @MapKeyColumn(name = "LANGUAGE_LOCALIZATION_ID")
    private Map<Long, LanguageDataLocalization> localizations;
    
    @OneToMany(fetch = FetchType.LAZY, mappedBy = "localizationLanguage")
    @MapKeyColumn(name = "LANGUAGE_ID")
    private Map<Long, LanguageDataLocalization> languagesLocalized;
    
    /**
     * Private no-arg constructor for reflection-based construction inside JPA providers.
     */
    @SuppressWarnings("unused")
    private LanguageData() {
        // Fields initialized by JPA.
    }
    
    // Plus getters for fields
    }
    

    My concept of the maps were supposed to contain was correct, but I wasn't thinking through how they were supposed to be keyed (off of the other column).