JPA @Onetoone with Shared Id -- How to Do This Better

JPA @OneToOne with Shared ID -- Can I do this Better?

To map one-to-one association using shared primary keys use @PrimaryKeyJoinColumn and @MapsId annotation.

Relevant sections of the Hibernate Reference Documentation:

PrimaryKeyJoinColumn

The PrimaryKeyJoinColumn annotation does say that the primary key of
the entity is used as the foreign key value to the associated entity.

MapsId

The MapsId annotation ask Hibernate to copy the identifier from
another associated entity. In the Hibernate jargon, it is known as a
foreign generator but the JPA mapping reads better and is encouraged

Person.java

@Entity
public class Person {

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

@OneToOne(cascade = CascadeType.ALL)
@PrimaryKeyJoinColumn
private VitalStats vitalStats;
}

VitalStats.java

@Entity
public class VitalStats
{
@Id @Column(name="vitalstats_id") Long id;

@MapsId
@OneToOne(mappedBy = "vitalStats")
@JoinColumn(name = "vitalstats_id") //same name as id @Column
private Person person;

private String stats;
}

Person Database Table

CREATE TABLE  person (
person_id bigint(20) NOT NULL auto_increment,
name varchar(255) default NULL,
PRIMARY KEY (`person_id`)
)

VitalStats Database Table

CREATE TABLE  vitalstats 
(
vitalstats_id bigint(20) NOT NULL,
stats varchar(255) default NULL,
PRIMARY KEY (`vitalstats_id`)
)

@OneToOne Mapping with Hibernate shared primary key User - Account

You can achieve this more simply in JPA 2+ as follows:

public class User{

@Id
@GeneratedValue(strategy=GenerationType.IDENTITY)
@Column(unique=true, nullable=false)
private int id;

@OneToOne(cascade=CascadeType.PERSIST, mappedBy = "user")
private Account account;
}

public class Account{

@Id
@OneToOne
@JoinColumn(name = "user_id")
private User user;
}

See:

https://en.wikibooks.org/wiki/Java_Persistence/Identity_and_Sequencing#Primary_Keys_through_OneToOne_and_ManyToOne_Relationships

JPA @Id on @OneToOne Entity with a Composite Key

This is what JPA calls a "derived identity". You might try something like this:

ReportDetails:

@Entity
public class ReportDetails implements Serializable {
// all attributes map by the relationship: AttributeOverride is not allowed
@EmbeddedId
private MachineLine.Id id;

@MapsId
@JoinColumns({
@JoinColumn(name="machineId", referencedColumnName="machineId"),
@JoinColumn(name="machineLineIndex", referencedColumnName="index")
})
@OneToOne
private MachineLine machineLine;

// ...
}

MachineLine:

@Entity
public class MachineLine {

@EmbeddedId
private Id id;

@MapsId("machineId") // maps machineId attribute of embedded id
@ManyToOne
private Machine machine;

// ...

@Embeddable
public static class Id implements Serializable {
private long machineId; // corresponds to PK type of Machine
private long index;
// ...
}
}

Machine:

@Entity
public class Machine {

@Id
private long id;

@OneToMany(mappedBy = "machine")
private List<MachineLine> lines;

// ...
}

Derived identities are discussed (with examples) in the JPA 2.2 spec in section 2.4.1.

OneToOne relationship with shared primary key generates n+1 selects; any workaround?

Imagine 2 tables in a relational database, e.g. Person and Billing. There is a (non-mandatory) OneToOne association defined between these entities,

Lazy fetching is conceptually not possible for non-mandatory OneToOne by default, Hibernate has to hit the database to know if the association is null or not. More details from this old wiki page:

Some explanations on lazy loading (one-to-one)


[...]

Now consider our class B has
one-to-one association to C

class B {
private C cee;

public C getCee() {
return cee;
}

public void setCee(C cee) {
this.cee = cee;
}
}

class C {
// Not important really
}

Right after loading B, you may call
getCee() to obtain C. But look,
getCee() is a method of YOUR class
and Hibernate has no control over it.
Hibernate does not know when someone
is going to call getCee(). That
means Hibernate must put an
appropriate value into "cee"
property at the moment it loads B from
database. If proxy is enabled for
C, Hibernate can put a C-proxy
object which is not loaded yet, but
will be loaded when someone uses it.
This gives lazy loading for
one-to-one.

But now imagine your B object may or
may not have associated C
(constrained="false"). What should
getCee() return when specific B
does not have C? Null. But remember,
Hibernate must set correct value of
"cee" at the moment it set B
(because it does no know when someone
will call getCee()). Proxy does not
help here because proxy itself in
already non-null object.

So the resume: if your B->C mapping
is mandatory (constrained=true),
Hibernate will use proxy for C
resulting in lazy initialization. But
if you allow B without C, Hibernate
just HAS TO check presence of C at the
moment it loads B. But a SELECT to
check presence is just inefficient
because the same SELECT may not just
check presence, but load entire
object. So lazy loading goes away
.

So, not possible... by default.

Is there a workaround for this potential performance disaster (other than not using a shared primary key at all)? Thank you for all your ideas.

The problem is not the shared primary key, with or without shared primary key, you'll get it, the problem is the nullable OneToOne.

First option: use bytecode instrumentation (see references to the documentation below) and no-proxy fetching:

@OneToOne( fetch = FetchType.LAZY )
@org.hibernate.annotations.LazyToOne(org.hibernate.annotations.LazyToOneOption.NO_PROXY)

Second option: Use a fake ManyToOne(fetch=FetchType.LAZY). That's probably the most simple solution (and to my knowledge, the recommended one). But I didn't test this with a shared PK though.

Third option: Eager load the Billing using a join fetch.

Related question

  • Making a OneToOne-relation lazy

References

  • Hibernate Reference Guide

    • 19.1.3. Single-ended association proxies
    • 19.1.7. Using lazy property fetching
  • Old Hibernate FAQ

    • How do I set up a 1-to-1 relationship as lazy?
  • Hibernate Wiki

    • Some explanations on lazy loading (one-to-one)

Unidirectional one-to-one relationship with shared primary key and @OneToOne on child side

That can be done with the help of MapsId:

Designates a ManyToOne or OneToOne relationship attribute that
provides the mapping for an EmbeddedId primary key, an attribute
within an EmbeddedId primary key, or a simple primary key of the
parent entity.

In this case appliance should be annotated with MapsId:

@OneToOne
@JoinColumn(name="applianceId")
@MapsId
private Appliance appliance;

Also nullable=false was dropped, because by definition primary key is not nullable.

One to One unidirectional mapping with same primary key in hibernate

You can change your mapping with @mapsId as suggested by Hadi J to have a single pk/fk column in RowCount

@Entity
@Table(name = "row_count")
public class RowCount implements Serializable {

@OneToOne
@JoinColumn(name = "file_id")
@MapsId
private MetaFile file;

@Id
@Column(name="file_id")
private int file_id;

private int rows;

public RowCount(MetaFile file, int rows) {
this.file = file;
this.rows = rows;
}
// getters, setters...

}

I would use a bidirectional relationship to simplify saving:

@Entity
@Table(name = "file")
public class MetaFile {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "file_id")
private int fileId;
// getters, setters, constructors...

@OneToOne(cascade = {CascadeType.ALL}, mappedBy = "file")
private RowCount rowCount;
}

This way you can just set the relationship and save

RowCount rowCount = new RowCount(metaFile, getNumberOfRows(file));
metaFile.setRowCount(rowCount);
metaFile = fileRepository.save(metaFile);

Change OneToOne link into another entity with another id

packingRepository.findById(id)
.ifPresent(packing -> packing.setPackingDictionary(packingDictionaryRepository.getOne(dictionaryId))

Do self referencing entities need @OneToOne JPA annotations or not?

No you always will need the annotation. If it is optional:

@OneToOne(optional = true)
private DepositAccountTransaction destinationTransaction;

I wouldn't declare any property as public btw. You may want to use projectlombok to generate getters and setters.

How to map one to one relation with no foreign key in JPA?

What you are looking for is bidirectional one to one mapping. Now the mapping is done only one way. In bidirectional mapping, you need to put File reference in FileContent and vice versa. Check the following code snippet -

File:

@Entity
@Table(name="FILE", schema="COMMON")
public class File {

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

@Column(name = "FILE_NAME")
private String fileName;

@OneToOne(mappedBy = "file", cascade = CascadeType.ALL,
fetch = FetchType.LAZY, optional = false)
private FileContent details;

//constructors getters and setters

}

FileContent:

@Entity
@Table(name="FILE_CONTENT", schema="COMMON")
public class FileContent{

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

@Column(name = "FILE_NAME")
private String fileName;

@OneToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "FILE_ID")
private File file;

//constructors getters and setters
}

Notes:

  • The above code snippet will create the table named FILE_CONTENT with a column named FILE_ID.
  • FILE_ID is the foreign key referencing the primary key of FILE.ID.

  • Here in the association, the File entity is the parent, while the FileContent is the child. Because the foreign key is located in the FILE_CONTENT table.

  • Now from your code, you can access both entities from any side like - file.getFileContent() or fileContent.getFile()



Related Topics



Leave a reply



Submit