10

Two tables like:

CREATE TABLE foo (
  id INT PRIMARY KEY,
  x TEXT
);

CREATE TABLE bar (
  foo_id INT REFERENCES foo (id) ON DELETE CASCADE,
  y TEXT,
  z TEXT
);

...can be mapped like so:

@Table(name = "foo")
@SecondaryTable(name = "bar", pkJoinColumns = @PrimaryKeyJoinColumn(name = "foo_id", referencedColumnName = "id"))
class Foo {

  @Id
  int id;

  @Embedded
  @AttributeOverrides({
    @AttributeOverride(name = "y", column = @Column(table = "bar")),
    @AttributeOverride(name = "z", column = @Column(table = "bar"))
  })
  Bar bar;
}

@Embeddable
class Bar {
  String y;
  String z;
}

Is there a less awkward way to do this mapping, using either just standard JPA annotations, or else Hibernate-specific annotations (and without introducing a parent reference in the embeddable object)?

Compare this to how easily a collection of @Embeddable objects can be referenced using an @ElementCollection and @CollectionTable.

ejain
  • 3,456
  • 2
  • 34
  • 52
  • Can't you simply map it using @ElementCollection but hide that implementation detail i.e. don;t expose the collection but provide set/getBar methods that manipulate the collection? – Alan Hay Apr 25 '17 at 08:22
  • That would work, too, but would be even uglier than the verbose annotations above :-) – ejain Apr 25 '17 at 16:22

1 Answers1

5

Sorry for the late answer, but the question interested me so I figured I'd take a crack at it.

A more elegant solution is to make the contents of bar an Entity instead of an Embeddable, and to use a OneToOne relationship instead of an Embedded. You also need to remember to use @PrimaryKeyJoinColumn instead of @JoinColumn:

@Entity
@Table(name = "bar")
class Bar {
  @Id
  int fooId;

  String y;
  String z;
}

...

@Entity
@Table(name = "foo")
class Foo {
  @Id
  int id;

  @OneToOne
  @PrimaryKeyJoinColumn
  Bar bar;
}

The biggest advantage is that Foo no longer needs to know about the contents of Bar. Depending on your DB's naming strategy, you may also need to specify the column name of fooId in Bar with something like @Column(name = "foo_id").

Alvin Thompson
  • 5,388
  • 3
  • 26
  • 39
  • 1
    Should have been more clear that the database schema is fixed (no unique identifiers for bar), so this solution wouldn't work. – ejain Jul 19 '18 at 20:31
  • @ejain in your DDL for Bar you have this: `foo_id INT REFERENCES foo (id) ON DELETE CASCADE`. That's all the unique identifier you need. If you're saying that there can be multiple rows in that table (bar) that refer to the same Foo entity, then even your original SecondaryTable solution wouldn't work. If the SecondaryTable solution works, so will this one. – Alvin Thompson Jul 19 '18 at 21:34
  • Good point, but looks `fooId` would have to be set explicitly? Might be able to use `@MapsId` (see https://stackoverflow.com/a/36528193/1144085), but that approach also has some drawbacks compared to using an `@Embeddable`. – ejain Jul 19 '18 at 23:56
  • @ejain I’ve tested this answer and it works perfectly for what you asked. It’s also exactly how what you asked is supposed to be done. The link you mentioned in your comment is not even the same issue. – Alvin Thompson Jul 20 '18 at 00:20
  • If you don’t want to mark this answer as correct for whatever reason, no one is forcing you. But my answer is exactly what you asked for. If you have a different problem now, you should post a new question, or at least modify this one. – Alvin Thompson Jul 20 '18 at 00:25
  • 1
    Your current solution loses the foreign key relationship, and only works as long as you manually set both primary keys to identical values... Just replace `@PrimaryKeyJoinColumn` with `@MapsId @JoinColumn(name = "id")`, and I'll accept your solution! – ejain Jul 20 '18 at 18:19
  • If you're talking about a foreign-key relationship in Bar linking to Foo, you never had this in your original problem or solution. – Alvin Thompson Jul 20 '18 at 20:48
  • If you now want one, you don't want to use `@MapsId` which is really only useful for embedded objects. Instead, you want to use the standard solution for when your primary key is also a foreign key. – Alvin Thompson Jul 20 '18 at 20:48
  • Simply also add `@Id` to the relationship. So instead of adding `@Id int fooId;`, you would add `@Id @OneToOne Foo foo;`. – Alvin Thompson Jul 20 '18 at 20:52
  • 1
    The original `@Embedded` solution creates a foreign key that matches the DDL also posted above; looks like using `@MapsId` allows us to have a foreign key without having to make the relationship bidirectional. – ejain Jul 20 '18 at 21:27
  • Your original embedded solution does *not* create a foreign key, and that's not what `@MapsId` does. No part of your previous statement is correct. – Alvin Thompson Jul 20 '18 at 22:04
  • What JPA implementation did you test this with? Hibernate (5.2.17) appears to create a foreign key constraint for both the `@Embedded` and the `@MapsId` solution: ```Hibernate: drop table bar if exists Hibernate: drop table foo if exists Hibernate: create table bar (value varchar(255), foo_id binary not null, primary key (foo_id)) Hibernate: create table foo (id binary not null, primary key (id)) Hibernate: alter table bar add constraint FKdvoqij212wwl2bf14kwo55h55 foreign key (foo_id) references foo``` – ejain Jul 20 '18 at 23:09
  • That’s the `@SecondaryTable` annotation that’s creating all of that. It has nothing to do with MapsId nor Embedded. Trust me, if you do as I say the Entity and Table annotations on Bar will create the Bar table, and the OneToOne will create the foreign key. Why don’t you actually try it and see if it works? Hint: it does. – Alvin Thompson Jul 20 '18 at 23:18
  • 1
    The `@MapsId` solution is based on your solution, and doesn't use `@Embedded` or `@SecondaryTable`. It's possible that this is a blatant misuse of this annotation, but it appears to work, and doesn't require changing the Bar domain object to have a reference back to a Foo object. – ejain Jul 20 '18 at 23:37
  • I was hoping that my original assessment was wrong, but it’s clear that I was spot on. I, like most people here, use SO to exchange ideas and try to help people in good faith. If you don’t want to accept my help, the only people you’re hurting are those who are searching for an answer to this question in the future. I wish you the best of luck. – Alvin Thompson Jul 21 '18 at 14:46
  • Here's an upvote for your troubles :-) Leaving the question open in case someone has a better solution that doesn't require changing the domain model or schema... – ejain Jul 22 '18 at 18:22