Are multiple natural identifier combinations possible in Hibernate? - java

Please, consider the following example:
import org.hibernate.annotations.NaturalId;
import javax.persistence.Column;
import javax.persistence.Table;
#javax.persistence.Entity
#Table(name = "Entity", uniqueConstraints = {
#javax.persistence.UniqueConstraint(columnNames = {"firstNaturalId"}),
#javax.persistence.UniqueConstraint(columnNames = {"secondNaturalIdPart1", "secondNaturalIdPart2"})
})
class Entity {
#NaturalId
#Column(name = "firstNaturalId")
private String firstNaturalId;
#NaturalId
#Column(name = "secondNaturalIdPart1")
private String secondNaturalIdPart1;
#NaturalId
#Column(name = "secondNaturalIdPart2")
private String secondNaturalIdPart2;
// The remainder is omitted.
}
The desired functionality is to be able to retrieve an identified uniquely entity either by providing ('firstNaturalId') or the ('secondNaturalIdPart1', 'secondNaturalIdPart2') group.
Is it possible in Hibernate to have several natural identifiers combinations (groups) that uniquely identify an entity within a table?

A long time has passed. Maybe you've got your answer. I had this question in my mind. Found your question with no answer. Searched more. Found this Natural Id Mapping.
public class Entity{
#NaturalId #Embedded
private EntityNI entityNI;
public Entity(){}
// Setter and Getter is omitted
// .
// .
// .
#Override
public int hashCode(){
return (entityNI != null ? entityNI.hashCode() : 31);
}
#Override
public boolean equals(final Object object){
if(this == object){
return true;
}
if( !(object instanceof Entity) || (getClass() != object.getClass()) ){
return false;
}
return this.entityNI.equals( ((Entity)object).entityNI );
}
}
And the embedded natural ids:
#Embeddable
public class EntityNI{
#NotBlank #Basic(optional = false) #Column(name = "firstNaturalId")
private String firstNaturalId;
#NotBlank #Basic(optional = false) #Column(name = "secondNaturalIdPart1")
private String secondNaturalIdPart1;
#NotBlank #Basic(optional = false) #Column(name = "secondNaturalIdPart2")
private String secondNaturalIdPart2;
public EntityNI(){}
// Setters and Getters are omitted
// .
// .
// .
#Override
public int hashCode(){
return Objects.hash(firstNaturalId, secondNaturalIdPart1, secondNaturalIdPart2);
}
#Override
public boolean equals(Object object){
if(this == object){
return true;
}
if( !(object instanceof EntityNI) || (getClass() != object.getClass()) ){
return false;
}
final EntityNI other = (EntityNI) object;
return (this.firstNaturalId == other.firstNaturalId) && (this.secondNaturalIdPart1 == other.secondNaturalIdPart1) && (this.secondNaturalIdPart2 == other.secondNaturalIdPart2);
}
}

Related

Overriding equals() and hashcode() methods in JPA embeddable does not work

JDK 17
SpringBoot latest
JPA latest
MySQL 8.0.31
I am trying to implement a strategy that makes sure that both the name and the email address of each user are unique.
User entity:
#Entity
public class User {
......
#EmbeddedId
protected UserId id;
......
}
User id:
#Embeddable
public class UserId implements Serializable {
#Serial
private static final long serialVersionUID = -622156255674132106L;
#Column(name = "name", nullable = false)
protected String name = "";
#Column(name = "email", nullable = false)
protected String email = "";
public UserId(String name, String email) {
setName(name);
setEmail(email);
}
public UserId() {}
public void setName(String name) {
this.name = name;
}
public String getName() {
return Objects.requireNonNullElse(name, "");
}
public void setEmail(String email) {
this.email = email;
}
public String getEmail() {
return Objects.requireNonNullElse(email, "");
}
}
Now, by default, it is marked as a conflict only if userA.name == userB.name && userA.email == userB.email, which means there can be two users having the same email address as long as they do not share one single name. How to stop this from happening? What I expect is userA.name == userB.name || userA.email == userB.email.
I've tried overriding equals() and hashcode() in the following way.
#Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof UserId userId)) return false;
if (Objects.equals(name, userId.name)) return true;
return Objects.equals(email, userId.email);
}
#Override
public int hashCode() {
int result = name != null ? name.hashCode() : 0;
result = 31 * result + (email != null ? email.hashCode() : 0);
return result;
}
However, it does not work. Also, breakpoints inside these two functions are not reached.
==========Edited==========
I've solved the original problem. But when it comes to UsersRepository.existsById(), it's still considered not to exist if either of the two columns does not match. How can I fix it?
Whether you do this via annotations and schema generation, or just by creating / modifying the schema directly, the answer is the same.
You will need to create a single unique constraint in the database naming both columns, not two separate constraints.
If you want a schema generation annotation to do this, supply the #UniqueConstraint annotation to the #Table annotation, e.g.
#Table(uniqueConstraints = {
#UniqueConstraint(columnNames = {
"name", "email"
})
})
public class UserId implements Serializable {
#Serial
private static final long serialVersionUID = -622156255674132106L;
#Column(name = "name", nullable = false, unique=true)
protected String name = "";
#Column(name = "email", nullable = false, unique=true)
protected String email = "";

Migrating from Hibernate 3.6 to 4.2: Hibernate auto generates foreign keys

As the title suggests: I am currently migrating from Hibernate 3.6 to 4.2.
PROBLEM
After the upgrade, Hibernate started to automatically generate a foreign key that points to the same table. Now, when trying to persist a new customer, a ConstraintViolationException is thrown.
DETAILS
Here's a picture of what I mean:
Here is the code of the corresponding class:
#Entity
#Table(name = "customers")
#Inheritance(strategy = InheritanceType.JOINED)
public abstract class Customer extends AbstractModel {
public Customer(final Provider provider) {
this.provider = provider;
}
#Required
#ManyToOne(cascade = CascadeType.REFRESH)
public Provider provider;
#MaxSize(1023)
public String note;
public String getNote() { return note; }
...
}
As you can see, it extends from AbstractModel which is a #MappedSuperclass and contains the id property which serves as a primary key for all of our model classes. It's a #GeneratedValue.
It's a superclass itself. I don't know whether it's important so I am just going to include the code and schema of one of its child classes:
And the corresponding code:
#Entity
#Table(name="unregistered_customers")
public class UnregisteredCustomer extends Customer {
#MaxSize(MAX_SIZE_OF_NAMES_AND_IDENTIFIERS)
#Column(nullable = false, length = MAX_SIZE_OF_NAMES_AND_IDENTIFIERS)
public String userName;
#Email
#MaxSize(MAX_SIZE_OF_EMAIL_ADDRESSES)
#Column(unique = false, length = MAX_SIZE_OF_EMAIL_ADDRESSES)
public String email;
#MaxSize(MAX_SIZE_OF_NAMES_AND_IDENTIFIERS)
#Column(length = MAX_SIZE_OF_NAMES_AND_IDENTIFIERS)
public String mobile;
public UnregisteredCustomer(final Provider provider) {
super(provider);
}
RESEARCH
I already looked into the migration guide but nothing in there seems to be related to my issue.
I also looked into the Hibernate documentation especially into the section dealing with #Inheritance. Sadly, I didn't find anything concerning auto generated foreign keys.
QUESTION
How do I stop Hibernate from adding this self-referencing foreign key?
EDIT 1
As requested, the super class:
#MappedSuperclass
public abstract class AbstractModel extends AbstractBaseModel {
#Id
#GeneratedValue
public Long id;
public Long getId() {
return id;
}
#Override
public Object _key() {
return id;
}
}
Also, the super class of the super class:
#MappedSuperclass
public abstract class AbstractBaseModel extends GenericModel {
public static final int MAX_SIZE_OF_NAMES_AND_IDENTIFIERS = 80;
public static final int MAX_SIZE_OF_COMMENTS_AND_DESCRIPTIONS = 5000;
public static final int MAX_LIST_SIZE = 30;
public static final int MAX_SIZE_OF_EMAIL_ADDRESSES = 255;
public static final int MAX_SIZE_OF_JSON_CONTENT_FIELDS = 65535;
#Column(nullable = false, unique = true)
public String uuid;
#Column(nullable = false)
public Long created;
public DateTime getCreatedAsDate() {
return DateUtil.dateTimeWithSystemTimezone(created);
}
#Column(nullable = false)
public Long lastModified;
#PrePersist
protected void prePersist() {
final Long nowInMillis = DateUtil.dateTimeWithSystemTimezone().getMillis();
if (uuid == null) {
uuid = UuidUtil.newUUIDAsString();
Logger.trace("Created new Uuid for entity: %s", uuid);
}
if (created == null) {
created = nowInMillis;
}
if (lastModified == null) {
lastModified = nowInMillis;
}
}
#PreUpdate
protected void preUpdate() {
lastModified = DateUtil.dateTimeWithSystemTimezone().getMillis();
}
#Override
public boolean equals(final Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
final AbstractBaseModel other = (AbstractBaseModel) obj;
return new EqualsBuilder().append(uuid, other.uuid).isEquals();
}
#Override
public int hashCode() {
return new HashCodeBuilder().append(uuid).toHashCode();
}
}
Put #Inheritance(strategy = InheritanceType.JOINED) to the parent entity (AbstractModel) not to the child. And customize the foreign key constraint using #PrimaryKeyJoinColumn at your child entity. Check out Part 4 (Joined Table) at https://www.baeldung.com/hibernate-inheritance to see it more clearly!

Composite Key - Hibernate

I have a table GROCERY which has following structure:
CREATE TABLE grocery
(
gro_id NUMBER,
gro_name VARCHAR(32),
gro_dep_name VARCHAR(32),
gro_price NUMBER(16, 2),
gro_max_discount NUMBER(16, 2),
CONSTRAINT gro_pk PRIMARY KEY (gro_id, gro_dep_name)
)
My problem is that, when I am trying to fetch the data from the table (saved in my oracle data base) , I am getting the following error :
org.hibernate.id.IdentifierGenerationException: null id generated
for:class com.domain.Grocery
I have generated following entity classes according to the structure of the table :
Grocery.java
package com.domain;
import java.math.BigDecimal;
import javax.persistence.AttributeOverride;
import javax.persistence.AttributeOverrides;
import javax.persistence.Column;
import javax.persistence.EmbeddedId;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Table;
/**
* Grocery generated by hbm2java
*/
#SuppressWarnings("serial")
#Entity
#Table(name = "GROCERY", schema = "TPRDBA")
public class Grocery implements java.io.Serializable {
#EmbeddedId
private GroceryId id;
private String groName;
private BigDecimal groPrice;
private BigDecimal groMaxDiscount;
public Grocery() {
}
public Grocery(GroceryId id) {
this.id = id;
}
public Grocery(GroceryId id, String groName, BigDecimal groPrice, BigDecimal groMaxDiscount) {
this.id = id;
this.groName = groName;
this.groPrice = groPrice;
this.groMaxDiscount = groMaxDiscount;
}
#EmbeddedId
#AttributeOverrides({
#AttributeOverride(name = "groId", column = #Column(name = "GRO_ID", nullable = false, precision = 22, scale = 0)),
#AttributeOverride(name = "groDepName", column = #Column(name = "GRO_DEP_NAME", nullable = false, length = 32)) })
public GroceryId getId() {
return this.id;
}
public void setId(GroceryId id) {
this.id = id;
}
#Column(name = "GRO_NAME", length = 32)
public String getGroName() {
return this.groName;
}
public void setGroName(String groName) {
this.groName = groName;
}
#Column(name = "GRO_PRICE", precision = 16)
public BigDecimal getGroPrice() {
return this.groPrice;
}
public void setGroPrice(BigDecimal groPrice) {
this.groPrice = groPrice;
}
#Column(name = "GRO_MAX_DISCOUNT", precision = 16)
public BigDecimal getGroMaxDiscount() {
return this.groMaxDiscount;
}
public void setGroMaxDiscount(BigDecimal groMaxDiscount) {
this.groMaxDiscount = groMaxDiscount;
}
}
GroceryId.java
package com.domain;
// Generated Nov 12, 2018 11:42:16 AM by Hibernate Tools 4.3.1.Final
import java.math.BigDecimal;
import javax.persistence.Column;
import javax.persistence.Embeddable;
/**
* GroceryId generated by hbm2java
*/
#SuppressWarnings("serial")
#Embeddable
public class GroceryId implements java.io.Serializable {
private BigDecimal groId;
private String groDepName;
public GroceryId() {
}
public GroceryId(BigDecimal groId, String groDepName) {
this.groId = groId;
this.groDepName = groDepName;
}
#Column(name = "GRO_ID", nullable = false, precision = 22, scale = 0)
public BigDecimal getGroId() {
return this.groId;
}
public void setGroId(BigDecimal groId) {
this.groId = groId;
}
#Column(name = "GRO_DEP_NAME", nullable = false, length = 32)
public String getGroDepName() {
return this.groDepName;
}
public void setGroDepName(String groDepName) {
this.groDepName = groDepName;
}
public boolean equals(Object other) {
if ((this == other))
return true;
if ((other == null))
return false;
if (!(other instanceof GroceryId))
return false;
GroceryId castOther = (GroceryId) other;
return ((this.getGroId() == castOther.getGroId()) || (this.getGroId() != null && castOther.getGroId() != null
&& this.getGroId().equals(castOther.getGroId())))
&& ((this.getGroDepName() == castOther.getGroDepName())
|| (this.getGroDepName() != null && castOther.getGroDepName() != null
&& this.getGroDepName().equals(castOther.getGroDepName())));
}
public int hashCode() {
int result = 17;
result = 37 * result + (getGroId() == null ? 0 : this.getGroId().hashCode());
result = 37 * result + (getGroDepName() == null ? 0 : this.getGroDepName().hashCode());
return result;
}
}
I have followed this example.
Please help me out, I am not able to figure out what is wrong in it.
Following is my service to take the data from database, which has GroceryRepository which extends CrudRepository :
#Service
public class GroceryService {
#Autowired
GroceryRepository groceryRepository;
public List<Grocery> getAllGrocery()
{
List<Grocery> groceries = new ArrayList<>();
groceryRepository.findAll().forEach(groceries::add);
return groceries;
}
public void addGrocery(Grocery grocery)
{
groceryRepository.save(grocery);
}
}
Missed #EmbeddedId annotation in Grocery.java. Update your code as below.
#EmbeddedId
private GroceryId id;
Just use #EmbeddedId.There must be only one EmbeddedId annotation and no Id annotation when the EmbeddedId annotation is used.
#EmbeddedId
private GroceryId id;

Criteria.DISTINCT_ROOT_ENTITY doesn't prevent duplicated objects

I have following dao method:
#Override
public List<AdminRole> findAll() {
Session session = sessionFactory.getCurrentSession();
Criteria criteria = session.createCriteria(AdminRole.class);
criteria.setResultTransformer(Criteria.DISTINCT_ROOT_ENTITY);
return criteria.list();
}
Actually I want to retrieve all entries from database.
Sometimes I see duplicates. This happens when I add user with AdminRole.
I have read that it is possible when I use EAGER fetch type and this should be fix adding following line:
criteria.setResultTransformer(Criteria.DISTINCT_ROOT_ENTITY);
But this doesn't help me.
my mapping:
#Entity
#Table(name = "terminal_admin_role")
public class AdminRole {
#Id
#Column(name = "role_id", nullable = false, unique = true)
#GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "user_id")
#SequenceGenerator(name = "user_id", sequenceName = "user_id")
private Long adminId;
#Column(name = "role")
private String role;
public AdminRole(String role) {
this.role = role;
}
public AdminRole() {
}
// get set
#Override
public String toString(){
return role;
}
#Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof AdminRole)) {
return false;
}
AdminRole adminRole = (AdminRole) o;
if (!role.equals(adminRole.role)) {
return false;
}
return true;
}
#Override
public int hashCode() {
return role.hashCode();
}
}
and
#Entity
#Table(name = "terminal_admin")
public class TerminalAdmin {
#ManyToMany(fetch=FetchType.EAGER,cascade=CascadeType.ALL)
#JoinTable(name = "admin_role", joinColumns = {
#JoinColumn(name = "admin_id", nullable = false) },
inverseJoinColumns = { #JoinColumn(name = "role_id",
nullable = false) })
private Set<AdminRole> adminRoles;
//...
}
P.S.
I cannot switch fetch type.
I don't want to put this list into set.
There is no reason to use DISTINCT_ROOT_ENTITY or anything similar, all you need is:
session.createCriteria(AdminRole.class).list();
If you get duplicates, then you really have them in the database. Check the code which saves AdminRoles either directly or by cascading from other entities.
When cascading PERSIST/MERGE operations from other entities, make sure that the operation is cascaded to a persistent/detached AdminRole instance, not to a transient (new) one.
My money's on messing with hashCode/equals overrides and Hibernate proxies.
From EqualsandHashCode
However, once you close the Hibernate session, all bets are off. [...] Hence, if you keep collections of objects around between sessions, you will start to experience odd behavior (duplicate objects in collections, mainly).
First I'd use org.apache.commons.lang3 like so (it's obviously way too expensive for Hibernate entities but works ok w/ them and if it works that should validate my hunch):
import org.apache.commons.lang3.builder.EqualsBuilder;
import org.apache.commons.lang3.builder.HashCodeBuilder;
import org.apache.commons.lang3.builder.ToStringBuilder;
#Override
public String toString() {
return ToStringBuilder.reflectionToString(this);
}
#Override
public int hashCode() {
return HashCodeBuilder.reflectionHashCode(this);
}
#Override
public boolean equals(Object other) {
return EqualsBuilder.reflectionEquals(this, other);
}
If this works you could go with a less expensive approach like so:
#Override
public int hashCode() {
HashCodeBuilder hcb = new HashCodeBuilder();
hcb.append(role);
return hcb.toHashCode();
}
#Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (!(obj instanceof AdminRole)) {
return false;
}
AdminRole that = (AdminRole) obj;
EqualsBuilder eb = new EqualsBuilder();
eb.append(role, that.role);
return eb.isEquals();
}

Insert to JPA collection without loading it

I'm currently using code like this to add a new entry to a set in my entity.
player = em.find(Player.class, playerId);
player.getAvatarAttributeOwnership().add(new AvatarAttributeOwnership(...));
It works, but every time I want to add one item, the whole set is loaded.
Is there a way (with a query maybe) to add the item without loading the rest? In SQL it would be something like INSERT INTO AvatarAttributeOwnership(player, data, ...) VALUES({player}, ...);
Currently uniqueness is maintained by the contract of Set and AvatarAttributeOwnership.equals, but I assume that won't work anymore. How can I enforce it anyway?
I'm using JPA2+Hibernate. Code:
#Entity
public class Player implements Serializable {
#Id
#GeneratedValue
private long id;
#ElementCollection(fetch=FetchType.LAZY)
// EDIT: answer to #2
#CollectionTable(uniqueConstraints=#UniqueConstraint(columnNames={"Player_id","gender","type","attrId"}))
Set<AvatarAttributeOwnership> ownedAvatarAttributes;
...
}
#Embeddable
public class AvatarAttributeOwnership implements Serializable {
#Column(nullable=false,length=6)
#Enumerated(EnumType.STRING)
private Gender gender;
#Column(nullable=false,length=20)
private String type;
#Column(nullable=false,length=50)
private String attrId;
#Column(nullable=false)
private Date since;
#Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null) return false;
if (getClass() != obj.getClass()) return false;
AvatarAttributeOwnership other = (AvatarAttributeOwnership) obj;
if (!attrId.equals(other.attrId)) return false;
if (gender != other.gender) return false;
if (!type.equals(other.type)) return false;
return true;
}
...
}
Try extra-lazy collections:
#LazyCollection(LazyCollectionOption.EXTRA)

Categories

Resources