How to map JPA OneToMany relationship when FK is part of CompositeId - java

So I have a pre-existing Service Entity with multiple OneToMany relationships. Now I need to add one more but I am having trouble and I assume it must be because the Many side uses a Composite Key.
I have the Service.java with its new fields
#Column(name = "TRANSLATION_DV_ID")
private String translationDvId;
#OneToMany(cascade = CascadeType.All, fetch = FetchType.Eager, mappedBy = "service")
private List<Translation> translation;
and
#IdClass(TranslationId.class)
public class Translation {
#Id
#Column(name = "TRANSLATION_DV_ID")
private String translationDvId;
#Id
#Column(name = "LOCALE_CD")
private String localeCd;
#Column(name = "TRANSLATED_NAME")
private String translatedName;
#Column(name = "TRANSLATED_DESC")
private String translatedDesc;
#ManyToOne
#JoinColumn(name = "TRANSLATION_DV_ID", insertable = false, updatable = false)
private Service service;
The test data is generated with sql scripts. I entered the new data and matched the translationDvId's. The data is all present with the correct information except for Translation relationship- each Service always has an empty List<Translation>.
I am not sure what I am missing but here is an example of a data entry
INSERT INTO SCHEMA.SERVICE(SERVICE_CD, TRANSLATION_DV_ID, etc, etc)
VALUES ('servicePrimaryKey', '12345', 'etc, 'etc);
INSERT INTO SCHEMA.TRANSLATION(TRANSLATION_DV_ID, LOCALE_CD, TRANSLATED_NAME, TRANSLATED_DESC)
VALUES ('12345', 'English', 'Guardian', 'Cool stuff');
INSERT INTO SCHEMA.TRANSLATION(TRANSLATION_DV_ID, LOCALE_CD, TRANSLATED_NAME, TRANSLATED_DESC)
VALUES ('12345', 'Spanish', 'Guardia', 'Cosas interesantes');

#JoinColumn has a special property for when there is a Composite PK in the referenced table - referencedColumnName
#OneToMany
#JoinColumn(name = "TRANSLATION_DV_ID", referencedColumnName = "TRANSLATION_DV_ID")
private List<Translation> translation;

Related

Circumventing Attribute Limitations of Many-to-Many Relations in JPA

I am using PostgreSQL 12.11, JPA 3.1.0, and Hibernate 5.6.10. This might become important because I am doing things that apparently do not work with JPA 2.0.
My goal is to add an attribute to a many-to-many relationship. I found this posting. #Mikko Maunu states that "There is no concept of having additional persistent attribute in relation in JPA (2.0)." To me, this sounds like what I want to do is not possible. However, the answer is rather old and might not be complete anymore.
Beside the time gap and the version gap, this is, in my opinion, a new question because I am doing something that is probably questionable and not part of the original thread.
What I did is this:
Create a #ManyToMany relationship in JPA and specify a #JoinTable.
Manually define an entity with identical table name to the table specified in 1. For this table, I chose a composite primary key using #IdClass. I also added my attribute.
Inside one of the n:m-connected entities, create a #OneToMany relationship to the connection-table-entity created in 2. However, I did not create a corresponding #ManyToOne relationship as that would have created an error.
As a result, I can access the original entities and their relation as many-to-many, but also the relation itself, which is not an entity in the original ERM, but it is for JPA. First tests show this seems to be working.
I am aware, however, that I basically access the same part of the persistence (the PostgreSQL database) through two different ways at the same time.
Now my questions are:
Is this a valid way to do it? Or will I get in bad trouble at one point?
Is there a situation where I will need to refresh to prevent trouble?
Is this something new in JPA > 2.0, or just an extension to the original answer?
This should help.
Here is how I do it:
#Entity
#Table(name = "person", schema = "crm")
public final class Person implements Serializable {
#Id
#Column(name = "id", unique = true, nullable = false, updatable = false, columnDefinition = "bigserial")
private Long id;
#OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY, mappedBy = "person", orphanRemoval = true)
private Set<PersonEmail> emails = new HashSet<>();
}
#Entity
#Table(name = "email", schema = "crm")
public final class Email implements Serializable {
#Id
#Column(name = "id", unique = true, nullable = false, updatable = false, columnDefinition = "bigserial")
private Long id;
#Column(name = "email", nullable = false, length = 64, columnDefinition = "varchar(64)")
private String localPart;
#Column(name = "domain", nullable = false, length = 255, columnDefinition = "varchar(255)")
private String domain;
}
#Entity
#Table(name = "person_email", schema = "crm")
public final class PersonEmail implements Serializable {
#EmbeddedId
private PersonEmailId id;
// The mapped objects are fetched lazily.
// This is a choice.
#ToString.Exclude
#MapsId("personId")
#ManyToOne(fetch = FetchType.LAZY, optional = false)
private Person person;
#ToString.Exclude
#MapsId("emailId")
#ManyToOne(fetch = FetchType.LAZY, optional = false)
private Email email;
// Here's an extra column.
#Column(name = "type", nullable = false, columnDefinition = "email_type_t")
#Convert(converter = EmailType.EmailTypeConverter.class)
private EmailType type;
public final void setPerson(final Person person) {
this.person = person;
id.setPersonId(this.person.getId());
}
public final void setEmail(final Email email) {
this.email = email;
id.setEmailId(this.email.getId());
}
#Embeddable
public static final class PersonEmailId implements Serializable {
#Column(name = "person_id", nullable = false, insertable = false, updatable = false, columnDefinition = "bigint")
private Long personId;
#Column(name = "email_id", nullable = false, insertable = false, updatable = false, columnDefinition = "bigint")
private Long emailId;
}

How to avoid updating child tables when using save Hibernate?

I have a parent table called USER and a child table called USERDATA linked with OneToMany. When I go to use the save method, if the child record exists it is updated. I would like it not to be updated but not added. What am I doing wrong?
My classes:
#Entity
#Table(name="USER")
#IdClass(UserPK.class)
public class User implements Serializable{
private static final long serialVersionUID = 1L;
#Id
#Column(name="USERID")
private String userId;
#Id
#Column(name="USERNUMBER")
private String userNumber;
private String name;
private String surname;
#OneToMany(mappedBy = "user", fetch = FetchType.EAGER, cascade = CascadeType.ALL)
private List<UserData> userDatas;
#OneToMany(mappedBy = "user", fetch = FetchType.EAGER, cascade = CascadeType.ALL)
#Fetch(value = FetchMode.SUBSELECT)
private List<OtherData> otherDatas;
//getter and setter
}
UserData:
#Entity
#Table(name="USERDATA")
public class UserData implements Serializable{
private static final long serialVersionUID = 1L;
#Id
private String id;
private String City;
private String University;
#ManyToOne
#JoinColumns({
#JoinColumn(name = "USERID", referencedColumnName = "USERID"),
#JoinColumn(name = "USERNUMBER", referencedColumnName = "USERNUMBER")
})
private User user;
//getter and setter
OtherData:
#Entity
#Table(name="OTHERDATA")
public class OtherData implements Serializable{
private static final long serialVersionUID = 1L;
#Id
private String id;
private String hobby;
private String religion;
#ManyToOne
#JoinColumns({
#JoinColumn(name = "USERID", referencedColumnName = "USERID"),
#JoinColumn(name = "USERNUMBER", referencedColumnName = "USERNUMBER")
})
private User user;
//getter and setter
And my save:
session.save(obj); //obj is and User object
when I do the first insert everything is ok, when I do it again by changing the data of the primary key of user but not quelly of the primary key of userData or otherData, the data is updated. I don't want them updated.
You can not reuse the same Java object in this case. Also, changing the primary key should produce a big fat warning when flushing that object Hibernate.
If you want a new row to be inserted, you have to create a new object with new User(). Another option is to let Hibernate forget about the old object by using session.detach(user). The problem is, that Hibernate knows the object already and due to that, tries to update the existing row when you call save again.
Try setting the column like so:
#OneToMany(mappedBy = "user", fetch = FetchType.EAGER, cascade = CascadeType.ALL)
#Column(insertable = false, updatable = false)
private List<UserData> userDatas;
I haven't tested it but it should block inserts and updates via the entity. Of course you will need separate queries to insert update UserData somehow.
BTW I suggest not to use FetchType.EAGER, but always use FetchType.LAZY. If you need the child data to be fetched, just adapt the query you are using to do a FETCH JOIN, see here for an example:
SELECT FROM User u LEFT JOIN FETCH u.userData d
Hi Fesilox Please write this example
#JoinColumn(name = "USERID", referencedColumnName = "USERID", nullable = false,
insertable=false, updatable=false)

HIbernate ignore fetching data from OnetoMany field

I would like to ignore #OnetoMany field in my entity. fetch data need to get actual fields but don't want to fire query to dependent table. But deleting data from parent table needs deletion from dependent table
I have tried #Transient that ignores but the delete is also being ignored. Is there any other option to tell JPA not to fetch data from childs table when i call the parent entity?
#Entity
Table(name = "User")
public class UserEntity implements Serializable {
#Id
#Column(name = "id")
private int id;
#Column(name = "SERIAL", unique = true, nullable = false)
private String serial;
#OneToMany(mappedBy = "serialBySerialId", cascade = CascadeType.ALL)
private Set<UserActionEntity> userActionsById;
}
#Table(name = "user_action")
public class UserActionEntity implements Serializable {
#Id
#Column(name = "id")
private int id;
#Column(name = "action")
private String action;
#ManyToOne
#JoinColumn(name = "USER_ID", referencedColumnName = "ID", nullable = false)
private UserEntity userByUserId;
If you don't want to fire query to dependent table, you can use (fetch = FetchType.LAZY) on UserActionEntity property.
#OneToMany(mappedBy = "serialBySerialId", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
private Set<UserActionEntity> userActionsById;

Hibernate avoid nullable unique fields in query?

I am having one hibernate pojo class which has 3 fields specified in #UniqueConstraint (unique together) where one of these 3 fields is nullable=true.
When I try to update entry with session.update(pojo) it updates all the entries in database which matches 2 fields (which are not nullable), so does hibernate avoid nullable fields while querying? or there is something what I should know about it?
Edit: Added class
#Entity
#Table (name = "details",
uniqueConstraints = {#UniqueConstraint(columnNames = {"service_id", "billing_item_id", "service_type_id"}, name="UK_name_it")}
)
public class Detail implements Serializable {
#ManyToOne
#JoinColumn(name = "service_id")
#ForeignKey(name = "FK_name2")
#Id
private Service service;
#ManyToOne
#JoinColumn(name="billing_item_id")
#ForeignKey(name = "FK_name3")
#Id
private BillingItem billingItem;
#ManyToOne
#JoinColumn(name="currency_id")
#ForeignKey(name = "FK_name4")
private Currency currency;
#ManyToOne
#JoinColumn(name="service_type_id")
#ForeignKey(name = "FK_name5")
private ServiceType serviceType;
#Column(name = "completed", nullable = false)
private boolean completed;
}
There doesn't seem to be any option like that to have a nullable field in composite key, so I had to end up by adding a integer autoincrement primary key to the table, and keeping service, billingItem and serviceType fields in #UniqueConstraint.
There is another option I could adopt, which is possible in certain scenarios, by adding a serviceType which is considered as All entry (basically when serviceType is null it applies to all the serviceTypes.) and instead of using null for serviceType point to this entry, this way we can have PK and no need to make serviceType a nullable field.

JPA merge results in duplicate entry of foreign entity

QUESTIONS:
Does anyone know how to merge without having EntityManager trying to re-insert the foreign entity?
SCENARIO:
Just to set up a scenario that closely matches my case: I have two entities
#Entity
#Table(name = "login", catalog = "friends", uniqueConstraints =
#UniqueConstraint(columnNames = "username"))
public class Login implements java.io.Serializable{
private static final long serialVersionUID = 1L;
#Id
#GeneratedValue(strategy = IDENTITY)
#Column(name = "id", unique = true, nullable = false)
private Integer id;
#Column(name = "username", unique = true, nullable = false, length = 50)
private String username;
#Column(name = "password", nullable = false, length = 250)
private String password;
}
#Entity
#Table(name = "friendshiptype", catalog = "friends")
public class FriendshipType implements java.io.Serializable{
private static final long serialVersionUID = 1L;
#Id
#GeneratedValue(strategy = IDENTITY)
#Column(name = "id", unique = true, nullable = false)
private Integer id;
#OneToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "username")
private Login login;
#Column(name = "type", unique = true, length = 32)
private String type;
...//other fields go here
}
Both the Login entity and the FriendshipType entity are persisted to the database separately. Then, later, I need to merge a Login row with a FriendshipType row. When I call entityManager.merge(friendship), it tries to insert a new Login which of course results in the following error
Internal Exception: com.mysql.jdbc.exceptions.jdbc4.MySQLIntegrityConstraintViolationException: Duplicate entry 'myUserName1350319637687' for key 'username'
Error Code: 1062
Call: INSERT INTO friends.login (password, username) VALUES (?, ?)
My question, again, is how do I merge two objects without having enityManager trying to reinsert the foreign object?
Here is how I solve the problem. I finally figure the reason the merge is not resolving is because the login.id is auto generated by JPA. So since I really don't need an auto-generated id field, I remove it from the schema and use username as the #id field:
#Entity
#Table(name = "login", catalog = "friends", uniqueConstraints =
#UniqueConstraint(columnNames = "username"))
public class Login implements java.io.Serializable{
private static final long serialVersionUID = 1L;
#Id
#Column(name = "username", unique = true, nullable = false, length = 50)
private String username;
#Column(name = "password", nullable = false, length = 250)
private String password;
}
Another solution that occurred to me, which I didn't implement but may help someone else, should they need to have an auto-generated id field.
Instead of creating an instance of Login for the merger, get the instance from the database. What I mean is, instead of
Login login = new Login(); login.setUsername(username); login.setPassword(password);
Do rather
Login login = loginDao.getByUsername(username);
That way, a new id field is not generated making the entity seem different.
Thanks and up-votes to everyone for helping, especially to #mijer for being so patient.
You can make your #JoinColumn non updatable:
#JoinColumn(name = "login_id", updatable = false) // or
#JoinColumn(name = "username", referencedColumnName = "username", updatable= false)
Or try to refresh / fetch your Login entity again before merging the FriendshipType:
// either this
entityManager.refresh(friendship.getLogin());
// or this
final Login login = entityManager
.getReference(Login.class, friendship.getLogin().getId());
friendship.setLogin(login);
// and then
entityManager.merge(friendship);
But, as other suggested I belive that FriendshipType would be better represented by a #ManyToOne relationship or maybe by a Embeddable or ElementCollection
Update
Yet another option is to change the owning side:
public class Login implements java.io.Serializable {
#OneToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "friendshiptype_id")
private FriendshipType friendshipType;
// Other stuff
}
public class FriendshipType implements java.io.Serializable {
#OneToOne(fetch=FetchType.LAZY, mappedBy="friendshipType")
private Login login;
// Other stuff
}
This will affect your data model (login table will have a friendshiptype_id column instead of the other way around), but will prevent the errors that you are getting, since relationships are always maintained by the owning side.
Have you tried cascade=MERGE? I.e.
#OneToOne(fetch = FetchType.LAZY, cascade=CascadeType.MERGE)
#JoinColumn(name = "username")
private Login login;
UPDATE
Another possible option is to use #ManyToOne (it's save as the association is unique)
#ManyToOne(fetch = FetchType.LAZY, cascade=CascadeType.MERGE)
#JoinColumn(name = "username")
private Login login;
You can do it with your original #Id setup. i.e.
#Id
#GeneratedValue(strategy = IDENTITY)
#Column(name = "id", unique = true, nullable = false)
private Integer id;
You can, but you don't need to change to:
#Id
#Column(name = "username", unique = true, nullable = false, length = 50)
private String username;
The trick is you must start by loading from the DB, via em.find(...) or em.createQuery(...). Then the id is guaranteed to be populated with the right value from the DB.
Then you can detach the entity by ending a transaction (for a transaction-scoped entity manager in a session bean), or by calling em.detach(ent) or em.clear(), or by serialising the entity and passing it over the network.
Then you can update the entity, all the while, keeping the original id value.
Then you can call em.merge(ent) and you will still have the correct id. However, I believe the entity must already pre-exist in the persistent context of the entity manager at this instant, otherwise it will think that you have a new entity (with manually populated id), and try to INSERT on transaction flush/commit.
So the second trick is to ensure the entity is loaded at the point of the merge (via em.find(...) or em.query(...) again, if you have a new persistent context and not the original).
:-)

Categories