tags:

views:

153

answers:

4

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.

+1  A: 

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.

wds
Further examination revealed this didn't work, because now hibernate is trying to join the String column in the association table with the id column of the entity. It seems this simple is impossible to do, somehow.
wds
A: 

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.

Stefan Haberl
I think you might be right. In fact I can change part of the schema, so I could in theory redo the join table to use IDs, but it would be even more work and so I ended up just using a workaround that seems to work for now.
wds
A: 

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.

Willi
You're defining two unidirectional many-to-many associations here, this is semantically very different (and I'm not even sure everything would work correctly).
Pascal Thivent
They are actually one relation because i use the same join table and the same join columns (just switched).
Willi
A: 

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();
        }

    }

}
Arthur Ronald F D Garcia
actually that is exactly what I did. I'm accepting this because the code might help others. :-)
wds
@wds Thank you! I hope it can be useful
Arthur Ronald F D Garcia