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.)
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).