Bidirectional OneToMany relationship with composite entity key cause null identifier - java

I am struggling with mapping a one-to-many bidirectional relationship with composite primary keys based on entites. I have created some classes that represents what I want to do. They are just made so it is easier to explain, but I think they communicate what I am trying to do well.
I have left out implementing some interfaces/methods, but I can ensure you that it works when I don't add the set of tournaments to the Player/Game entity. So something is wrong with my mapping.
When I try to find an entity (pretty much do anything) Hibernate 4.x cause a null identifier error saying nothing more about the error.
What would be the correct way to map this when I have a composite primary key on the "one" and I want bidirectional navigation?
#Entity
#IdClass(TournamentPk.class)
public class Tournament {
#Id
#ManyToOne
#JoinColumn("player_id")
private Player player;
#Id
#ManyToOne
#JoinColumn("game_id")
private Game game;
private int score;
// Getters and Setters
// Hashcode and Equals
}
// Primary key class.
public class TournamentPk {
private Player player;
private Game game;
// Getters and Setters
// Hashcode and Equals
}
#Entity
public class Player {
// ID
// Other fields
#OneToMany(fetchType = FetchType.EAGER, mappedBy = "player")
private Set<Tournament> tournaments;
// Getters and Setters
// Hashcode and Equals
}
#Entity
public class Game {
// ID
// Other fields
#OneToMany(fetchType = FetchType.EAGER, mappedBy = "game")
private Set<Tournament> tournaments;
// Getters and Setters
// Hashcode and Equals
}

Your problem is that you annotated with #Id fields of not-allowed types (e.g of type Player and Game). See the documentation of the #Id annotation for details
Note for completeness: some references allow to have another Entity class annotated with #Id.
In your place I would have an independent #Id private Long id; field in your Tournament entity + an #Unique constraint on the Game+Player combination with a standard mapping.
If you really want to stick with #IdClass, you might try adding two allowed fields in your tournament class (I have found a similar mapping here (search for #IdClass)).
#Entity
#IdClass(TournamentPk.class)
public class Tournament {
#Id
#Column(name="player_id", insertable = false, updatable = false)
private Long playerId;
#ManyToOne
#JoinColumn("player_id")//consider adding cascade=PERSIST
private Player player;
#Id
#Column(name="game_id", insertable = false, updatable = false)
private Long gameId;
#ManyToOne
#JoinColumn("game_id")//consider adding cascade=PERSIST
private Game game;
private int score;
// Getters and Setters
// Hashcode and Equals
}
public class TournamentPk {
private Long playerId;
private Long gameId;
// Getters and Setters
// Hashcode and Equals
}

Related

How do I cascade persist an #OneToMany relationship with an #EmbeddedId using Spring Data / Hibernate

I've seen a lot of similar questions asked about this but haven't found a solution that fixes the problem I'm seeing, so apologies up front if this is a redundant question. In my situation I have various types of entities and they're each going to have their own tag associations. So I want a generic Tag class that won't have it's own id, but rather an id / composite key made of the id of the entity it's tagging, plus the tag type. To (attempt to) achieve this I made an #Embeddable id class:
#Embeddable
public class TagId implements Serializable {
#Column(columnDefinition = "BINARY(16)")
private UUID parentId;
private String value;
// Getters, setters...
}
That Id is in turn used by a #MappedSuperclass:
#MappedSuperClass
public class Tag {
#EmbeddedId
private TagId id;
// Other attributes, getters, setters...
}
... and then when I want to tag a specific entity, for example using a BookTag, the table would have a book_id column as a foreign key to a Book table taking the place of parentId :
#Entity
#Table(name = "book_tag")
#AttributeOverride(name = "parentId", column = #Column(name = "book_id"))
public class BookTag extends Tag {
// other attributes, getters, setters...
}
Then finally, I have a Book entity:
#Entity
#Table(name = "book")
public class Book {
#Id
#GeneratedValue
#Column(columnDefinition = "Binary(16)")
private UUID id;
// other attributes, getters, setters...
#OneToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL, mappedBy = "id.parentId")
private List<BookTag> tags;
}
When I then try to save a new Book, with a populated BookTag collection, using a Spring Data JPA repository to repo.save(book), my desired behavior is that the Book is saved, then the id is copied to the BookTag objects, and those are saved. Unfortunately, what I'm seeing in the log is that Book is inserted as expected, then the inserts for the Tag objects are run, but book_id is being bound as null for each of the entries.
I've tried a few other approaches:
#JoinColumn instead of mappedBy
#MapsId with a #ManyToOne reference to Book on BookTag
#GeneratedValue on parentId
None worked, but it is possible my syntax was off. Thanks in advance for anyone who knows how to tackle this problem.
To anyone who wants to do something similar, I finally found a solution that meets my criteria.
TagId was modified to this:
#Embeddable
public class TagId<T> implements Serializable {
#ManyToOne
private T taggedEntity;
private String value;
// Getters, setters...
}
...which leads to a slight modification to Tag...
#MappedSuperClass
public class Tag<T> {
#EmbeddedId
private TagId id;
// Other attributes, getters, setters...
}
...and then BookTag...
#Entity
#Table(name = "book_tag")
public class BookTag extends Tag<Book> {
// other attributes, getters, setters...
}
...and finally Book:
#Entity
#Table(name = "book")
public class Book {
#Id
#GeneratedValue
#Column(columnDefinition = "Binary(16)")
private UUID id;
// other attributes, getters, setters...
#OneToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL, mappedBy = "id.taggedEntity")
private List<BookTag> tags;
}
Now I can add 1...* BookTags to a Book, and in turn I have to set the Book on all the BookTags, but then it's one call to bookRepository.save() and everything cascades down. It would have been nicer to just do it with an id, but a generic is flexible enough. I'll just have it implement an interface so that toString/hashCode/equals can call getId on parent.
The only other drawback is I couldn't get #AttributeOverride to work, so while I'd prefer that my BookTag table have a book_id column, tagged_entity_id will have to suffice.

#MappedSuperclass : Persistent entity should have primary key

I'm working with Spring Data JPA and I'm trying to create 4 different entities that will have exactly the same fields but they will be stored in 4 different tables.
This is my key class
public class IndexId implements Serializable {
private int seqNo;
private String index;
// getters and setters
}
Then I have the base class:
#MappedSuperclass
public class BaseIndex {
#Id
#Column(name = "seq_no", nullable = false)
protected int seqNo;
#Id
#Column(name = "index", nullable = false)
protected String index;
#Column(name = "value", nullable = false)
protected String value;
//getters/setters
}
Then my entity that will store in the database:
#Entity
#IdClass(IndexId.class)
#Table(name = "bibliographic_single_index")
public class BibliographicSingleIndex extends BaseIndex implements Serializable { }
This is the error I get: Persistent entity 'BibliographicSingleIndex' should have primary key .
I also tried with the properties declared as private and the articles I see on this subject seem to do the same thing.
With these pieces of code is it possible to identify what I'm doing wrong?
I believe every entity needs a separate java class for the id class. You wouldn't have the problem with embedded ids I think.

Problems with Relationship Composite Primary Key on Hibernate

I have this diagram
How can I create the relationship on Hibernate of the entities
EscalaDetalhes and DefinicaoEscala, because on DefiniciaoEscala entity there is a composite primary key formed by idDetalhe (foreign with (EscalaDetalhes)) and idTurma(foreign with (Turma)).
I've already done the class like below :
#Embeddable
public class DefinicaoEscalaPK implements Serializable {
#Column(name="myColumn")
private Integer idTurma;
#Column(name="myColumn2")
private Integer idEscalaDia;
//GETTERS , SETTERS , HASH AND EQUALS
}
#Entity
public class DefinicaoEscala implements Serializable {
#EmbeddedId
private DefinicaoEscalaPK id;
#ManyToOne()
#JoinColumn(name = "idTurno")
private TurnoBean turno;
//GETTERS , SETTERS , HASH AND EQUALS
}
#Entity
#Table(name ="table")
public class EscalaDetalhes{
#id
private Integer idDetalhe;
#ManyToOne()
#JoinColumn(name="mybdColumn")
private EscalaBean escala;
#Column(name="myColumn")
private Calendar dia;
//MY QUESTION IS HERE , HOW WOULD I DESCRIBE THE RELATION HERE?
private List<DefinicaoEscala> escalaDiaDetalhes;
//GETTERS , SETTERS , HASH AND EQUALS
}
My question is here, How can I describe the relation?
private List<DefinicaoEscala> escalaDiaDetalhes;
referencedColumnName is basically used to convey that which column of the other entity will be mapped could be used for mapping in the current entity, thus, you can use the property referencedColumnName while mentioning the relationship:
#Entity
#Table(name ="table")
public class EscalaDetalhes{
#id
private Integer idDetalhe;
#ManyToOne()
#JoinColumn(name="mybdColumn")
private EscalaBean escala;
#Column(name="myColumn")
private Calendar dia;
//MY QUESTION IS HERE , HOW WOULD I DESCRIBE THE RELATION HERE?
#OneToMany
#JoinColumn(name="idDetalhe",referencedColumnName="idEscalaDia")
private List<DefinicaoEscala> escalaDiaDetalhes;
//GETTERS , SETTERS , HASH AND EQUALS
}
P.S: It is entirely on the basis of code you have shared.

Hibernate criteria on embedded id member member value

I would like to find an entity using a critera with restriction on the value of an attribute of a second entity wich is a member of the embedded id of my first entity.
First entity :
#Entity
public class Car {
#EmbeddedId
private Id id = new Id();
private String color;
#Embeddable
public static class Id implements Serializable {
private static final long serialVersionUID = -8141132005371636607L;
#ManyToOne
private Owner owner;
private String model;
// getters and setters...
// equals and hashcode methods
}
// getters and setters...
}
Second entity :
#Entity
public class Owner {
#Id
#GeneratedValue (strategy = GenerationType.AUTO)
private Long id;
private String firstname;
private String lastname;
#OneToMany (mappedBy = "id.owner")
private List<Car> cars;
// getters and setters...
}
In this example, I would like to obtain the car with the color 'black', model 'batmobile' and the owner's firstname 'Bruce' (oops... spoiler ;) )
I tried to do something like that but it won't work :
List<Car> cars = session.createCriteria(Car.class)
.add(Restrictions.eq("color", "black"))
.add(Restrictions.eq("id.model", "batmobile"))
.createAlias("id.owner", "o")
.add(Restrictions.eq("o.firstname", "Bruce"))
.list();
Result :
Hibernate: select this_.model as model1_0_0_, this_.owner_id as owner_id3_0_0_, this_.color as color2_0_0_ from Car this_ where this_.color=? and this_.model=? and o1_.firstname=?
ERROR: Unknown column 'o1_.firstname' in 'where clause'
What is the right way to obtain what I want ?
update
I tried in hql :
String hql = "FROM Car as car where car.color = :color and car.id.model = :model and car.id.owner.firstname = :firstname";
Query query = em.createQuery(hql);
query.setParameter("color", "black");
query.setParameter("model", "batmobile");
query.setParameter("firstname", "Bruce");
List<Car> cars = query.getResultList();
It works but is there a way to do this with criteria ?
You forgot to add the #Column annotation on top of the firstname and lastname fields (and the color field in Car). In hibernate if a field is not annotated, it doesn't recognize it as a database field. This page should give you a good idea about how to set up your model objects.
NOTE: You can have the column annotation over the getters and be fine, but you didn't show the getters. Either place is fine.
Look at what HQL is spitting back out, specifically the statement (formated for easier reading):
select
this_.model as model1_0_0_,
this_.owner_id as owner_id3_0_0_,
this_.color as color2_0_0_
from Car this_
where
this_.color=?
and this_.model=?
and o1_.firstname=?
It looks like hibernate is translating the field "id.owner" to "o" as your alias told it to to, but for some reason it's not writing down that "id.owner=o" as intended. You may want to do some research into why it may be doing that.
As per https://hibernate.atlassian.net/browse/HHH-4591 there is a workaround.
You have to copy the needed relation-property of the #EmbeddedId (owner in this case) to the main entity (Car in this case) with insertable = false, updatable = false as follows
#Entity
public class Car {
#EmbeddedId
private Id id = new Id();
private String color;
#ManyToOne
#JoinColumn(name = "column_name", insertable = false, updatable = false)
private Owner owner;
#Embeddable
public static class Id implements Serializable {
private static final long serialVersionUID = -8141132005371636607L;
#ManyToOne
private Owner owner;
private String model;
// getters and setters...
// equals and hashcode methods
}
// getters and setters...
}
Then just create directly the alias instead of using the composite id property
List<Car> cars = session.createCriteria(Car.class)
.add(Restrictions.eq("color", "black"))
.add(Restrictions.eq("id.model", "batmobile"))
.createAlias("owner", "o")
.add(Restrictions.eq("o.firstname", "Bruce"))
.list();

Hibernate failing by prepending fully qualified class name to property name on ManyToMany association

I'm trying to map two objects to each other using a ManyToMany association, but for some reason when I use the mappedBy property, hibernate seems to be getting confused about exactly what I am mapping. The only odd thing about my mapping here is that the association is not done on a primary key field in one of the entries (the field is unique though).
The tables are:
Sequence (
id NUMBER,
reference VARCHAR,
)
Project (
id NUMBER
)
Sequence_Project (
proj_id number references Project(id),
reference varchar references Sequence(reference)
)
The objects look like (annotations are on the getter, put them on fields to condense a bit):
class Sequence {
#Id
private int id;
private String reference;
#ManyToMany(mappedBy="sequences")
private List<Project> projects;
}
And the owning side:
class Project {
#Id
private int id;
#ManyToMany
#JoinTable(name="sequence_project",
joinColumns=#JoinColumn(name="id"),
inverseJoinColumns=#JoinColumn(name="reference",
referencedColumnName="reference"))
private List<Sequence> sequences;
}
This fails with a MappingException:
property-ref [_test_local_entities_Project_sequences] not found on entity [test.local.entities.Project]
It seems to weirdly prepend the fully qualified class name, divided by underscores. How can I avoid this from happening?
EDIT:
I played around with this a bit more. Changing the name of the mappedBy property throws a different exception, namely:
org.hibernate.AnnotationException: mappedBy reference an unknown target entity property: test.local.entities.Project.sequences
So the annotation is processing correctly, but somehow the property reference isn't correctly added to Hibernate's internal configuration.
I have done the same scenario proposed by your question. And, as expected, i get the same exception. Just as complementary task, i have done the same scenario but with one-to-many many-to-one by using a non-primary key as joined column such as reference. I get now
SecondaryTable JoinColumn cannot reference a non primary key
Well, can it be a bug ??? Well, yes (and your workaround works fine (+1)). If you want to use a non-primary key as primary key, you must make sure it is unique. Maybe it explains why Hibernate does not allow to use non-primary key as primary key (Unaware users can get unexpected behaviors).
If you want to use the same mapping, You can split your #ManyToMany relationship into #OneToMany-ManyToOne By using encapsulation, you do not need to worry about your joined class
Project
#Entity
public class Project implements Serializable {
#Id
#GeneratedValue
private Integer id;
#OneToMany(mappedBy="project")
private List<ProjectSequence> projectSequenceList = new ArrayList<ProjectSequence>();
#Transient
private List<Sequence> sequenceList = null;
// getters and setters
public void addSequence(Sequence sequence) {
projectSequenceList.add(new ProjectSequence(new ProjectSequence.ProjectSequenceId(id, sequence.getReference())));
}
public List<Sequence> getSequenceList() {
if(sequenceList != null)
return sequenceList;
sequenceList = new ArrayList<Sequence>();
for (ProjectSequence projectSequence : projectSequenceList)
sequenceList.add(projectSequence.getSequence());
return sequenceList;
}
}
Sequence
#Entity
public class Sequence implements Serializable {
#Id
private Integer id;
private String reference;
#OneToMany(mappedBy="sequence")
private List<ProjectSequence> projectSequenceList = new ArrayList<ProjectSequence>();
#Transient
private List<Project> projectList = null;
// getters and setters
public void addProject(Project project) {
projectSequenceList.add(new ProjectSequence(new ProjectSequence.ProjectSequenceId(project.getId(), reference)));
}
public List<Project> getProjectList() {
if(projectList != null)
return projectList;
projectList = new ArrayList<Project>();
for (ProjectSequence projectSequence : projectSequenceList)
projectList.add(projectSequence.getProject());
return projectList;
}
}
ProjectSequence
#Entity
public class ProjectSequence {
#EmbeddedId
private ProjectSequenceId projectSequenceId;
#ManyToOne
#JoinColumn(name="ID", insertable=false, updatable=false)
private Project project;
#ManyToOne
#JoinColumn(name="REFERENCE", referencedColumnName="REFERENCE", insertable=false, updatable=false)
private Sequence sequence;
public ProjectSequence() {}
public ProjectSequence(ProjectSequenceId projectSequenceId) {
this.projectSequenceId = projectSequenceId;
}
// getters and setters
#Embeddable
public static class ProjectSequenceId implements Serializable {
#Column(name="ID", updatable=false)
private Integer projectId;
#Column(name="REFERENCE", updatable=false)
private String reference;
public ProjectSequenceId() {}
public ProjectSequenceId(Integer projectId, String reference) {
this.projectId = projectId;
this.reference = reference;
}
#Override
public boolean equals(Object o) {
if (!(o instanceof ProjectSequenceId))
return false;
final ProjectSequenceId other = (ProjectSequenceId) o;
return new EqualsBuilder().append(getProjectId(), other.getProjectId())
.append(getReference(), other.getReference())
.isEquals();
}
#Override
public int hashCode() {
return new HashCodeBuilder().append(getProjectId())
.append(getReference())
.hashCode();
}
}
}
I finally figured it out, more or less. I think this is basically a hibernate bug.
edit: I tried to fix it by changing the owning side of the association:
class Sequence {
#Id
private int id;
private String reference;
#ManyToMany
#JoinTable(name="sequence_project",
inverseJoinColumns=#JoinColumn(name="id"),
joinColumns=#JoinColumn(name="reference",
referencedColumnName="reference"))
private List<Project> projects;
}
class Project {
#Id
private int id;
#ManyToMany(mappedBy="projects")
private List<Sequence> sequences;
}
This worked but caused problems elsewhere (see comment). So I gave up and modeled the association as an entity with many-to-one associations in Sequence and Project. I think this is at the very least a documentation/fault handling bug (the exception isn't very pertinent, and the failure mode is just wrong) and will try to report it to the Hibernate devs.
IMHO what you are trying to achieve is not possible with JPA/Hibernate annotations. Unfortunately, the APIDoc of JoinTable is a bit unclear here, but all the examples I found use primary keys when mapping join tables.
We had the same issue like you in a project where we also could not change the legacy database schema. The only viable option there was to dump Hibernate and use MyBatis (http://www.mybatis.org) where you have the full flexibility of native SQL to express more complex join conditions.
I run into this problem a dozen times now and the only workaround i found is doing the configuration of the #JoinTable twice with swapped columns on the other side of the relation:
class Sequence {
#Id
private int id;
private String reference;
#ManyToMany
#JoinTable(
name = "sequence_project",
joinColumns = #JoinColumn(name="reference", referencedColumnName="reference"),
inverseJoinColumns = #JoinColumn(name="id")
)
private List<Project> projects;
}
class Project {
#Id
private int id;
#ManyToMany
#JoinTable(
name = "sequence_project",
joinColumns = #JoinColumn(name="id"),
inverseJoinColumns = #JoinColumn(name="reference", referencedColumnName="reference")
)
private List<Sequence> sequences;
}
I did not yet tried it with a column different from the primary key.

Categories

Resources