I have the following "audit" table that can host audit information about any other class of my schema:
CREATE TABLE `audit` (
`table_name` varchar(45) NOT NULL,
`item_id` int(11) NOT NULL,
`version` int(11) NOT NULL,
`updated_at` datetime NOT NULL,
`updated_by` varchar(25) NOT NULL,
`comment` varchar(255) DEFAULT NULL,
PRIMARY KEY (`table_name`,`item_id`,`version`)
)
Then, I have different JPA entities in my schema like so:
#Entity(name = "EntityA")
#Table(name = "entity_a")
public class EntityA {
#Id
#GeneratedValue
private Long id;
private Long version;
// Other fields
#OneToOne(mappedBy = "id.item", targetEntity = EntityAAudit.class, fetch = FetchType.EAGER, cascade = CascadeType.ALL)
private EntityAAudit audit;
}
At the same time, I have an asbtract class Audit that is a super-class for multiple entity-specific Audit classes:
#MappedSuperclass
#Table(name = "audit")
#Inheritance(strategy = InheritanceType.SINGLE_TABLE)
#DiscriminatorColumn(name = "table_name", discriminatorType = DiscriminatorType.STRING)
#DiscriminatorOptions(insert = true, force = true)
public abstract class AuditHistory {
// Some audit fields like the date and the author of the modification
}
#Entity(name = "EntityAAudit")
#Table(name = "audit")
#Inheritance(strategy = InheritanceType.SINGLE_TABLE)
#DiscriminatorValue("entity_a")
#DiscriminatorOptions(insert = true, force = true)
public class EntityAAudit extends Audit {
#EmbeddedId
#JsonUnwrapped
private AuditedId id;
#Embeddable
public static class AuditedId implements Serializable {
#OneToOne
#JoinColumn(name = "item_id", nullable = false)
private EntityA item;
#Column(name = "version", nullable = false)
private Long version;
}
}
This mapping works when retrieving the entities and their audit information from the database, but not when inserting a new entity with the corresponding audit information:
EntityA entity = new EntityA();
entity.setVersion(/* ... */);
// Setting the other basic fields of the entity
EntityAAudit audit = new EntityAAudit();
// Setting the basic fields of the audit
entity.setAudit(audit);
entity = em.merge(entity);
I end up with the following exception:
org.hibernate.id.IdentifierGenerationException: null id generated for:class EntityAAudit
I have literally tried everything I could think of or find online, and in the end it always comes down to the same issue: Hibernate tries to insert my Audit object with empty values for the item_id and the version.
If I manually set my entity instance and the version as the id of the audit object like so:
EntityA entity = new EntityA();
entity.setVersion(/* ... */);
// Setting the other basic fields of the entity
EntityAAudit audit = new EntityAAudit();
// Setting the basic fields of the audit
audit.setId(new EntityAAudit.AuditedId());
audit.getId().setItem(entity);
audit.getId().setVersion(entity.getVersion());
entity.setAudit(audit);
entity = em.merge(entity);
Then I end up with the even more obscure error here:
Caused by: java.lang.NullPointerException
at org.hibernate.type.descriptor.java.AbstractTypeDescriptor.extractHashCode(AbstractTypeDescriptor.java:65)
at org.hibernate.type.AbstractStandardBasicType.getHashCode(AbstractStandardBasicType.java:185)
at org.hibernate.type.AbstractStandardBasicType.getHashCode(AbstractStandardBasicType.java:189)
at org.hibernate.type.EntityType.getHashCode(EntityType.java:348)
Note that I cannot change the structure of my database nor the version of Hibernate (5.1.0, I know some bugs are fixed in later versions that could solve my issue...).
Thanks a lot :)
You might try a "derived identity" mapping:
#Entity(name = "EntityAAudit")
#Table(name = "audit")
#Inheritance(strategy = InheritanceType.SINGLE_TABLE)
#DiscriminatorValue("entity_a")
#DiscriminatorOptions(insert = true, force = true)
public class EntityAAudit extends Audit {
#EmbeddedId
#JsonUnwrapped
private AuditedId id;
#OneToOne
#JoinColumn(name = "item_id", nullable = false)
#MapsId("entityAId") // maps entityAId attribute of embedded id
private EntityA item;
#Embeddable
public static class AuditedId implements Serializable {
private Long entityAId; // corresponds to PK type of EntityA
#Column(name = "version", nullable = false)
private Long version;
}
}
Note the #MapsId annotation on EntityAAudit.item.
Also, you will need to explicitly set EntityAAudit.item and AuditedId.version. JPA does not magically determine and set any circular references for you.
Derived identities are discussed (with examples) in the JPA 2.2 spec in section 2.4.1.
Related
I need some help/advice since I am new to Spring Boot. I am trying to make many-to-many relationship with the help of linking table. But for some reason, I can not persist data in the link table.
Here are the entities:
#Table(name = "providers")
#Getter
#Setter
#NoArgsConstructor
#AllArgsConstructor
public class ProviderEntity {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long providerId;
#OneToMany(mappedBy = "provider", fetch = FetchType.LAZY)
#JsonManagedReference
private List<ProviderPractitionersEntity> providerPractitioners;
...
-------------------------------------------------------------
#Entity
#Table(name = "company_practitioner_types")
#Getter
#Setter
#AllArgsConstructor
#NoArgsConstructor
public class CompanyPractitionerTypeEntity {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#OneToMany(mappedBy = "practitioner", fetch = FetchType.LAZY)
#JsonManagedReference
private List<ProviderPractitionersEntity> practitionerProviders;
...
}
---------------------------------------------------------------
#Entity
#Table(name = "provider_practitioners")
#Getter
#Setter
#NoArgsConstructor
#AllArgsConstructor
public class ProviderPractitionersEntity {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "id", nullable = false)
private Long id;
#ManyToOne
#JsonBackReference
#JoinColumn(name = "practitioner_id") /*this column is foreign key from practitioner table*/
private CompanyPractitionerEntity practitioner;
#ManyToOne
#JsonBackReference
#JoinColumn(name = "provider_id") /*this column is foreign key from provider table*/
private ProviderEntity provider;
#Column(name = "size")
private String size;
}
When I am persisting new Provider, I set this new provider object's reference and practitioner object's reference in every ProviderPractitioner object before persisting.
As the result, objects from List providerPractitioners have null values for all states. And nothing is persisted to provider_practitioners table in the database.
The reason I am trying to set many-to-many relationship this way, instead of using #ManyToMany annotation is because of the "size" variable in ProviderPractitionerEntity which contains number of one type of practitioners for one provider.
I have tried to create embededId (composite ID) for linking table, and got the same result.
********* UPDATE ***********
I created embededId class as suggested:
#Embeddable
#Getter
#Setter
#AllArgsConstructor
#NoArgsConstructor
#ToString
public class ProviderPractitionersId implements Serializable {
#Column(name = "practitioner_id")
private Long practitionerId;
#Column(name = "provider_id")
private Long providerId;
#Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof ProviderPractitionersId)) return false;
ProviderPractitionersId that = (ProviderPractitionersId) o;
return Objects.equals(getPractitionerId(), that.getPractitionerId()) && Objects.equals(getProviderId(), that.getProviderId());
}
#Override
public int hashCode() {
return Objects.hash(getPractitionerId(), getProviderId());
}
}
and added it to the join Entity:
#Entity
#Table(name = "provider_practitioners")
#Getter
#Setter
#NoArgsConstructor
#AllArgsConstructor
public class ProviderPractitionersEntity {
#EmbeddedId
private ProviderPractitionersId id;
#ManyToOne(fetch = FetchType.LAZY)
#MapsId("practitionerId")
#JoinColumn(name = "practitioner_id")
private CompanyPractitionerEntity practitioner;
#ManyToOne(fetch = FetchType.LAZY)
#MapsId("providerId")
#JoinColumn(name = "provider_id")
private ProviderEntity provider;
#Column(name = "size")
private String size;
}
This is my join table in DB
create table provider_practitioners
(
practitioner_id integer,
provider_id integer,
size varchar,
PRIMARY KEY (practitioner_id, provider_id),
FOREIGN KEY (practitioner_id) REFERENCES company_practitioners (id)
ON DELETE SET NULL,
FOREIGN KEY (provider_id) REFERENCES providers (provider_id)
ON DELETE SET NULL
);
I am setting values to each ProviderPractitionersEntity (Id, provider reference, practitioner reference, size value) Before, I did not set the ID object.
public Set<ProviderPractitionersEntity> dtoToPractitionersEntity(final ProviderDto providerDto,
final ProviderEntity providerEntity) {
final Set<ProviderPractitionersEntity> providerPractitionersEntities = new HashSet<>();
//Iterate through passed list of practitioner types for given provider
providerDto.getPractitioners().forEach(practitionerDTO -> {
final ProviderPractitionersEntity providerPractitioner = new ProviderPractitionersEntity();
//Check if there is current practitioner type in the codebook table with practitioner types
final CompanyPractitionerEntity practitioner = companyPractitionerRepository.findByPractitionerId(practitionerDTO).orElseThrow();
//add provider-practitioner set's reference to practitioner object
practitioner.setPractitionerProviders(providerPractitionersEntities);
//add current practitioner reference to ProviderPractitioner instance
providerPractitioner.setPractitioner(practitioner);
//add provider reference to ProviderPractitioner entity instance
providerPractitioner.setProvider(providerEntity);
//add values to the key and add key to the ProviderPractitioner instance
final ProviderPractitionersId providerPractitionersId = new ProviderPractitionersId(practitioner.getId(), providerEntity.getProviderId());
providerPractitioner.setId(providerPractitionersId);
//set size
providerPractitioner.setSize("5-10");
When I try to persist Provider object, now I get javax.persistence.EntityNotFoundException: Unable to find providers.model.entity.ProviderPractitionersEntity with id ProviderPractitionersId(practitionerId=2, providerId=0).
providerId is 0 at this point because provider object is not persisted yet. Why is it trying to fetch it? Is it because I set up the key value?
You have to create the #EmbeddedId as you mentioned, and then in the ProviderPractitionersEntity attributes (ProviderEntity and CompanyPractitionerTypeEntity) add #MapsId with the name of the property in the composite id.
So first create the composite id:
#Embeddable
public class ProviderPractitionersId implements Serializable {
#Column(name = "practitioner_id")
private Long practitionerId;
#Column(name = "provider_id")
private Long providerId;
And then, in the many-to-many entity (ProviderPractitionersEntity) map the id and both entities this way:
#EmbeddedId
private ProviderPractitionersId id;
#ManyToOne(fetch = FetchType.LAZY)
#MapsId("practitionerId")
private CompanyPractitionerTypeEntity practitioner;
#ManyToOne(fetch = FetchType.LAZY)
#MapsId("providerId")
private ProviderEntity provider;
And so, in the ProviderPractitionersEntity you can add, as you mentioned, as many properties as you like, like your size or anything else.
Update after comments
As you said, relations are automatically persisted or not depending on the CascadeType you specify. If none is specified you have to manually persist all entities (including the middle table) and at the correct order. First the Provider and the Practitioner have to exist in their tables in order for the ProviderPractitioner to get inserted, as this has foreign keys to those tables. So you either fine tune the CascadeType at your needs, or perform all the inserts manually in the correct order. That depends on your specific business cases.
Good day, here is problem:
CrudRepository returns wrong Id for entities.
Here is base JPA user entity:
#Data
#Entity(name = "user")
#EqualsAndHashCode(exclude = {"roles", "password", "data"})
#ToString(exclude = {"roles", "password", "data"})
public class User {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#OneToOne(mappedBy = "user", fetch = FetchType.LAZY)
private SomeData data;
...
There is a relation one-to-one to some data entity.
#Data
#Entity(name = "some_data")
#TypeDefs({
#TypeDef(name = "jsonb", typeClass = JsonBinaryType.class)
})
#ToString(exclude = {"user", "views", "biData"})
#EqualsAndHashCode(exclude = {"user", "views", "biData"})
public class SomeData {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#OneToOne
#MapsId
private User user;
...
And there is crud repository:
#Repository
public interface SomeDataRepository extends CrudRepository<SomeData, Long> {
Optional<SomeData> findByUserId(Long userId);
}
Method findUserById returns correct SomeData entity from DB, but this entity has the same ID with userId...
And because of it I can't do other activities (insert or update on table "public_view" violates foreign key constraint "fk_view_to_some_data")
It's quite strange.
The problem could be because you use the #MapsId annotation. Here's what the value of the annotation does, as per Javadoc:
The name of the attribute within the composite key to which the relationship attribute corresponds. If not supplied, the relationship maps the entity's primary key.
You could try to set a specific value to your annotation, or map differently your one-to-one relationship. For example, use the #JoinColumn annotation in your SomeData class:
// ... your annotations ...
public class SomeData {
// ... your other fields ...
#OneToOne
#JoinColumn(name = "some_data_id", referencedColumnName = "id")
private User user;
}
Here are some alternatives that you could use: https://www.baeldung.com/jpa-one-to-one
On my OneToOne relation I am getting:
NULL not allowed for column "USER_ID"; SQL statement:
insert into customer_order (id, enabled, orden_id) values (null, ?, ?).
It is actually null because is not present in the INSERT query. However, the value USER is filled in customerOder entity when perfom save(customerOrder).
#Getter
#SuperBuilder
#MappedSuperclass
#NoArgsConstructor
#EqualsAndHashCode(onlyExplicitlyIncluded = true)
public abstract class AbstractEntity {
#Id
#EqualsAndHashCode.Include
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#Setter
#Default
#Column(columnDefinition = "BOOLEAN DEFAULT 'true'", nullable = false)
private Boolean enabled = Boolean.TRUE;
}
#Getter
#Setter
#Entity
#Table(name = "customer_order")
#SuperBuilder
#NoArgsConstructor
#EqualsAndHashCode(callSuper = true, onlyExplicitlyIncluded = true)
public class CustomerOrderEntity extends AbstractEntity {
#OneToOne(fetch = FetchType.LAZY)
#MapsId("id")
private UserEntity user;
//other values
}
#Data
#Entity
#Table(name = "user")
#SuperBuilder
#NoArgsConstructor
#EqualsAndHashCode(callSuper = true, onlyExplicitlyIncluded = true)
public class UserEntity extends AbstractEntity {
#NaturalId
#EqualsAndHashCode.Include
#Column(length = 28, unique = true, nullable = false)
private String uuid;
//other values
}
I expect the customerOrder to be persisted in database with the filled data.
Using #MapsId allows you to use the child table Primary Key as a Foreign Key to the parent table Primary Key.
If you enable the hbm2ddl tool, you will see that the customer_order table will not contain the user_id column.
However, since you generated the database schema previously and you have a customer_order table with a dedicated user_id column, then you need to remove #MapsId:
#OneToOne(fetch = FetchType.LAZY)
private UserEntity user;
This way, the user association will use the user_id Foreign Key column.
Your code works exactly as you specified.
If you have a shared key (you use #MapsId), Hibernate won’t use a separate column for the foreign key. This is why the insert query does not contain user_id column.
On top if that, id in CustomerOrderEntity is on one hand auto-generated (as defined in the superclass), and on the other maps id of another entity. These are conflicting requirements.
I have an entity BlocRecord having a composite code BlocRecordId, and in its composite code there is an #Embedded (relation code ManyToOne) pointing to another entiy Record and want to Audit the entity BlocRecord.
The entity BlocRecord
#Entity
#Inheritance(strategy = InheritanceType.JOINED)
#Table(name = "blocRecord")
#Access(value = AccessType.FIELD)
#Audited
public class BlocRecord {
#EmbeddedId
private BlocRecordId blocRecordId = new BlocRecordId();
#ManyToOne(fetch = FetchType.LAZY, optional = false)
#JoinColumns({
#JoinColumn(name = "record_identifier_", referencedColumnName = "identifier_", unique = false, nullable = false),
#JoinColumn(name = "record_recordType_", referencedColumnName = "recordType_", unique = false, nullable = false)})
#MapsId("record")
private Record record;
...
}
The id class BlocRecordID
#Embeddable
public class BlocRecordId implements Serializable {
#Embedded
private RecordId record;
#Column(name = "source_")
String source ;
#Column(name = "messageType_")
String messageType ;
The entity Record
#Entity
#Inheritance(strategy = InheritanceType.JOINED)
#Table(name = "records")
#Access(value = AccessType.FIELD)
#Audited
public class Record {
#EmbeddedId
private RecordId recordId = new RecordId();
#OneToMany(targetEntity = BlocRecord.class, fetch = FetchType.LAZY, mappedBy = "record")
private Set<BlocRecord> blocRecord = new java.util.HashSet<>();
...
}
The idClass of the entity Record
#Embeddable
public class RecordId implements Serializable{
#Column(name = "identifier_")
String identifier ;
#Column(name = "recordType_")
String recordType ;
}
Hibernate-envers fails when trying to generate the metadata of the field record in the embeddable BlocRecordId, the The flowing exception is thrown
org.hibernate.MappingException: Type not supported: org.hibernate.type.ComponentType
at org.hibernate.envers.configuration.internal.metadata.IdMetadataGenerator.addIdProperties(IdMetadataGenerator.java:121)
at org.hibernate.envers.configuration.internal.metadata.IdMetadataGenerator.addId(IdMetadataGenerator.java:230)
at org.hibernate.envers.configuration.internal.metadata.AuditMetadataGenerator.generateFirstPass(AuditMetadataGenerator.java:642)
at org.hibernate.envers.configuration.internal.EntitiesConfigurator.configure(EntitiesConfigurator.java:95)
at org.hibernate.envers.boot.internal.EnversServiceImpl.doInitialize(EnversServiceImpl.java:154)
at org.hibernate.envers.boot.internal.EnversServiceImpl.initialize(EnversServiceImpl.java:118)
at org.hibernate.envers.boot.internal.AdditionalJaxbMappingProducerImpl.produceAdditionalMappings(AdditionalJaxbMappingProducerImpl.java:99)
at org.hibernate.boot.model.process.spi.MetadataBuildingProcess.complete(MetadataBuildingProcess.java:288)
at org.hibernate.boot.model.process.spi.MetadataBuildingProcess.build(MetadataBuildingProcess.java:83)
at org.hibernate.boot.internal.MetadataBuilderImpl.build(MetadataBuilderImpl.java:417)
at org.hibernate.boot.internal.MetadataBuilderImpl.build(MetadataBuilderImpl.java:86)
at org.hibernate.boot.MetadataSources.buildMetadata(MetadataSources.java:179)
Do you have any idea how to resolve the issue ?
Thanks
At the moment, Envers does not support the idea of nesting an embeddable inside an embeddable when we map the identifier columns like your example illustrates. The only valid mappings that Envers presently does support is if the attribute in the embeddable is a #ManyToOne or a #Basic type.
You can work around this problem but it involves being a bit more explicit and not using RecordId. What I mean is rewrite BlocRecordId to be the following:
#Embeddable
public class BlocRecordId implements Serializable {
#Column(name = "identifier_")
String identifier;
#Column(name = "recordType_")
String recordType;
#Column(name = "source_")
String source;
#Column(name = "messageType_")
String messageType;
#Transient
private RecordId recordId;
/** Helper method to assign the values from an existing RecordId */
public void setRecordId(RecordId recordId) {
this.identifier = recordId.getIdentifier();
this.recordType = recordId.getRecordType();
}
/** Helper method to get the RecordId, caching it to avoid multiple allocations */
public RecordId getRecordId() {
if ( recordId == null ) {
this.recordId = new RecordId( identifier, recordType );
}
return this.recordId;
}
}
I agree this is less than ideal but it does at least work around the current limitation of the code. I have gone added and added HHH-13361 as an open issue to support this. You're welcomed to contribute if you wish or I'll work in getting this supported for Envers 6.0.
I have two tables A & B with a foreign key from B to a called FK_A. I have a not null constraint on FK_A.
I have the following two classes:
#Entity
#Table(name = "A")
public class A {
#Id
#GeneratedValue(strategy = GenerationType.SEQUENCE)
#Column(name = "ID")
private long id;
#OneToMany(cascade=CascadeType.ALL)
#JoinColumn(name = "FK_A", nullable = true)
private Set<B> b;
//getters setters etc.
}
#Entity
#Table(name = "B")
public class B {
//attributes, getters setters etc.
}
The reads work fine but when I try to write A to the DB, I get : ORA-01400: cannot insert NULL into ("SCHEMA"."B"."FK_A")
The code I am tring to insert the entity looks something like:
#PersistenceContext(unitName = "ab")
private EntityManager em;
A a = new A();
B b = new B();
Set<B> bList = new HashSet();
bList.add(b);
a.setB(bList);
em.persist(a);
Now if I am correct shouldn't hibernate automatically populate FK_A in table B based on the id it had autogenerated. If not how can I go about setting it?
Take a look at mappedBy OneToMany.mappedBy(). JavaDoc:
The field that owns the relationship. Required unless the relationship is unidirectional.
It is a synonym for the Hibernate's keyword "inverse".
See also this question.
In addition as a variant may be your class B has property for class A declared with #ManyToOne(optional = false) annotation.