This is a follow-up question to my previous one How to model packages, versions and licenses?.
Here is my database setup.
V1__create_table_license.sql
CREATE TABLE IF NOT EXISTS license (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
reference TEXT NOT NULL,
is_deprecated_license_id BOOLEAN NOT NULL,
reference_number INTEGER NOT NULL,
license_id TEXT NOT NULL,
is_osi_approved BOOLEAN NOT NULL
);
INSERT INTO license
("name",reference,is_deprecated_license_id,reference_number,license_id,is_osi_approved)
VALUES
('MIT License','./MIT.json',false,275,'MIT',true);
V2__create_npm_package.sql
CREATE TABLE IF NOT EXISTS npm_package (
id BIGSERIAL PRIMARY KEY,
name TEXT NOT NULL,
description TEXT NOT NULL
);
INSERT INTO npm_package
(name, description)
VALUES
('react', 'React is a JavaScript library for building user interfaces.'),
('react-router-dom', 'DOM bindings for React Router'),
('typescript', 'TypeScript is a language for application scale JavaScript development'),
('react-dom', 'React package for working with the DOM.');
V3__create_npm_version.sql
CREATE TABLE IF NOT EXISTS npm_package_version (
npm_package_id BIGINT NOT NULL REFERENCES npm_package,
version TEXT NOT NULL,
license_id INTEGER NOT NULL REFERENCES license,
UNIQUE(npm_package_id, version)
)
Here are my Java objects.
License.java
#Entity
public class License {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
private String reference;
private Boolean isDeprecatedLicenseId;
private Integer referenceNumber;
private String name;
private String licenseId;
private Boolean isOsiApproved;
}
LicenseRepository.java
public interface LicenseRepository extends JpaRepository<License, Integer> {
License findByLicenseIdIgnoreCase(String licenseId);
}
NpmPackage.java
#Entity
public class NpmPackage {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String description;
#OneToMany(mappedBy = "npmPackage", cascade = CascadeType.ALL, orphanRemoval = true)
private List<NpmPackageVersion> versions = new ArrayList<>();
public NpmPackage() {}
public void addVersion(NpmPackageVersion version) {
this.versions.add(version);
version.setNpmPackage(this);
}
public void removeVersion(NpmPackageVersion version) {
this.versions.remove(version);
version.setNpmPackage(null);
}
}
#Entity
public class NpmPackageVersion {
public NpmPackageVersion() {}
public NpmPackageVersion(String version, License license) {
this.setVersion(version);
this.license = license;
}
#EmbeddedId private NpmPackageIdVersion npmPackageIdVersion = new NpmPackageIdVersion();
#MapsId("npmPackageId")
#ManyToOne(fetch = FetchType.LAZY)
private NpmPackage npmPackage;
#ManyToOne(fetch = FetchType.LAZY)
private License license;
#Embeddable
public static class NpmPackageIdVersion implements Serializable {
private static final long serialVersionUID = 3357194191099820556L;
private Long npmPackageId;
private String version;
// ...
}
public String getVersion() {
return this.npmPackageIdVersion.version;
}
public void setVersion(String version) {
this.npmPackageIdVersion.version = version;
}
}
MyRunner.java
#Component
class MyRunner implements CommandLineRunner {
#Autowired LicenseRepository licenseRepository;
#Autowired NpmPackageRepository npmPackageRepository;
#Override
// #Transactional
public void run(String... args) throws Exception {
// get license from database
var license = licenseRepository.findByLicenseIdIgnoreCase("mit");
// get package from db
var dbPackage = npmPackageRepository.findByNameIgnoreCase("react");
var version = new NpmPackageVersion("1.0.0", license);
dbPackage.addVersion(version);
npmPackageRepository.save(dbPackage);
}
}
In my previous question I got the answer to use fetch = FetchType.EAGER but then I learned that this is not ideal. I'd like to use lazy fetching.
#OneToMany(mappedBy = "npmPackage", fetch = FetchType.EAGER, cascade = CascadeType.ALL, orphanRemoval = true)
private List<NpmPackageVersion> versions = new ArrayList<>();
So I removed the eager fetching and run into an error.
org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: com.example.bom.NpmPackage.NpmPackage.versions, could not initialize proxy - no Session
With the #Transactional annotation everything works. Why is this the case? I tried to read everything online but I still don't really get it. I understand that the database session is closed at some point and I wonder where exactly this is the case. I also wonder if I could do something about, e.g. I tried to fetch all versions to ensure they are loaded before I add another one.
So do I really have to use #Transactional or is there another solution? I just want to understand the "magic" going on :)
Thank you very much!
When you use FetchType.LAZY, Hibernate ORM doesn't really return an initialized collection when you find the entity. The association is going to be a proxy and when you need access to the collection, Hibernate ORM is going to query the database and get it.
To achieve this, the entity (the NpmPackage) needs to be in a managed state. If the entity is not managed and you try to access a lazy association (versions in this case), you get the LazyInitializationException.
In your example, when you use #Transactional, the entity stays managed for the duration of the method. Without it, it becomes not managed as soon as you return from findByNameIgnoreCase.
If you know that you will need the association versions, you could also use a fetch join query to get the NpmPackage:
from NpmPackage p left join fetch p.versions where p.name=:name
This way the associations stays lazy but you can get it with a single query.
Related
In our spring boot application, I am trying to save an aggregate, that consists of a root entity (ParentEntity) and a Set of child entities (ChildEntity).
The intention is, that all operations are done through the aggreate. So there is no need for a repository for ChildEntity, as the ParentEntity is supposed to manage all save or update operations.
This is how the Entities look like:
#Entity
#Table(name = "tab_parent", schema = "test")
public class ParentEntity implements Serializable {
#Id
#Column(name = "parent_id")
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer parentId;
#Column(name = "description")
private String description;
#Column(name = "created_datetime", updatable = false, nullable = false)
#ColumnTransformer(write = "COALESCE(?,CURRENT_TIMESTAMP)")
private OffsetDateTime created;
#Column(name = "last_modified_datetime", nullable = false)
#ColumnTransformer(write = "COALESCE(CURRENT_TIMESTAMP,?)")
private OffsetDateTime modified;
#OneToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL, orphanRemoval = true, mappedBy = "ParentEntity")
private Set<ChildEntity> children;
// constructor and other getters and setters
public void setChildren(final Set<ChildEntity> children) {
this.children = new HashSet<>(children.size());
for (final ChildEntity child : children) {
this.addChild(child);
}
}
public ParentEntity addChild(final ChildEntity child) {
this.children.add(child);
child.setParent(this);
return this;
}
public ParentEntity removeChild(final ChildEntity child) {
this.children.add(child);
child.setParent(null);
return this;
}
}
#Entity
#DynamicUpdate
#Table(name = "tab_child", schema = "test")
public class ChildEntity implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "child_id")
private Integer childId;
#Column(name = "language_id")
private String languageId;
#Column(name = "text")
private String text;
#Column(name = "created_datetime", updatable = false, nullable = false)
#ColumnTransformer(write = "COALESCE(?,CURRENT_TIMESTAMP)")
public OffsetDateTime created;
#Column(name = "last_modified_datetime", nullable = false)
#ColumnTransformer(write = "COALESCE(CURRENT_TIMESTAMP,?)")
public OffsetDateTime modified;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "parent_id", updatable = false)
private ParentEntity parent;
// constructor and other getters and setters
public ParentEntity getParent() {
return this.parent;
}
public void setParent(final ParentEntity parent) {
this.parent = parent;
}
}
This is the store method to save or update the entities:
public Integer merge(final ParentDomainObject parentDomainObject) {
final ParentEntity parentEntity =
this.mapper.toParentEntity(parentDomainObject);
final ParentEntity result = this.entityManager.merge(parentEntity);
this.entityManager.flush();
return result.getParentId();
}
And this is the store method to retrieve the aggregate by id:
public Optional<ParentDomainObject> findById(final Integer id) {
return this.repo.findById(id).map(this.mapper::toParentDomainObject);
}
As you can see our architecture strictly separates the store from the service layer. So the service only knows about domain objects and does not depend on Hibernate Entites at all.
When updating either the child or the parent, firstly the parent is loaded. In the service layer, the domain object is updated (fields are set, or a child is added/removed).
Then the merge method (see code snippet) of the store is called with the updated domain object.
This works, but not completely as we want to. Currently every update leads to the parent and EVERY chhild entity being saved, even if all field remained the same. We added the #DynamicUpdate annotaton. Now we saw, that the "modified" field is the problem.
We use a #ColumnTransformer to have the database set the date. Now even if you call the services update method without changing anything, Hibernate generates a update query for EVERY object, which updates only the modified field.
The worst thing about that is, as every object is saved, every modified date changed as well to the current date. But we need information about exactly which object really changed and when.
Is there any way to tell hibernate, that this column should not be taken into account when deciding what to update. However of course, if a field changed, the update operation should indeed update the modified field.
UPDATE:
My second approach after #Christian Beikov mentioned the use of #org.hibernate.annotations.Generated( GenerationTime.ALWAYS )
is the following:
Instead of #Generated (which uses #ValueGenerationType( generatedBy = GeneratedValueGeneration.class )),
I created my own annotations, which use custom AnnotationValueGeneration implementations:
#ValueGenerationType(generatedBy = CreatedTimestampGeneration.class)
#Retention(RetentionPolicy.RUNTIME)
public #interface InDbCreatedTimestamp {
}
public class CreatedTimestampGeneration
implements AnnotationValueGeneration<InDbCreatedTimestamp> {
#Override
public void initialize(final InDbCreatedTimestamp annotation, final Class<?> propertyType) {
}
#Override
public GenerationTiming getGenerationTiming() {
return GenerationTiming.INSERT;
}
#Override
public ValueGenerator<?> getValueGenerator() {
return null;
}
#Override
public boolean referenceColumnInSql() {
return true;
}
#Override
public String getDatabaseGeneratedReferencedColumnValue() {
return "current_timestamp";
}
}
#ValueGenerationType(generatedBy = ModifiedTimestampGeneration.class)
#Retention(RetentionPolicy.RUNTIME)
public #interface InDbModifiedTimestamp {
}
public class ModifiedTimestampGeneration
implements AnnotationValueGeneration<InDbModifiedTimestamp> {
#Override
public void initialize(final InDbModifiedTimestamp annotation, final Class<?> propertyType) {
}
#Override
public GenerationTiming getGenerationTiming() {
return GenerationTiming.ALWAYS;
}
#Override
public ValueGenerator<?> getValueGenerator() {
return null;
}
#Override
public boolean referenceColumnInSql() {
return true;
}
#Override
public String getDatabaseGeneratedReferencedColumnValue() {
return "current_timestamp";
}
}
I use these annotations in my entities instead of the #ColumnTransformer annotations now.
This works flawlessly when I insert a new ChildEntity via addChild(), as now not all timestamps of all entities of the aggregate are updated anymore. Only the timestamps of the new child are set now.
In other words, the InDbCreatedTimestamp works as it should.
Sadly, the InDbModifiedTimestamp does not. Because of GenerationTiming.ALWAYS, I expected the timestamp to be generated on db level, everytime an INSERT OR UPDATE is issued. If I change a field of a ChildEntity and then save the aggregate, an update statement is generated only for this one database row, as expected. However, the last_modified_datetime column is not updated, which is surprising.
It seems that this is unfortunately still an open bug. This issue describes my problem precisely: Link
Can someone provide a solution how to get this db function executed on update as well (without using db triggers)
You could try to use #org.hibernate.annotations.Generated( GenerationTime.ALWAYS ) on these fields and use a database trigger or default expression to create the value. This way, Hibernate will never write the field, but read it after insert/update.
Overall this has a few downsides though (need the trigger, need a select after insert/update), so I think this is a perfect use case for Blaze-Persistence Entity Views.
I created the library to allow easy mapping between JPA models and custom interface or abstract class defined models, something like Spring Data Projections on steroids. The idea is that you define your target structure(domain model) the way you like and map attributes(getters) via JPQL expressions to the entity model.
A DTO/domain model for your use case could look like the following with Blaze-Persistence Entity-Views:
#EntityView(ParentEntity.class)
#UpdatableEntityView
public interface ParentDomainObject {
#IdMapping
Integer getParentId();
OffsetDateTime getModified();
void setModified(OffsetDateTime modified);
String getDescription();
void setDescription(String description);
Set<ChildDomainObject> getChildren();
#PreUpdate
default preUpdate() {
setModified(OffsetDateTime.now());
}
#EntityView(ChildEntity.class)
#UpdatableEntityView
interface ChildDomainObject {
#IdMapping
Integer getChildId();
String getName();
}
}
Querying is a matter of applying the entity view to a query, the simplest being just a query by id.
ParentDomainObject a = entityViewManager.find(entityManager, ParentDomainObject.class, id);
The Spring Data integration allows you to use it almost like Spring Data Projections: https://persistence.blazebit.com/documentation/entity-view/manual/en_US/index.html#spring-data-features
Page<ParentDomainObject> findAll(Pageable pageable);
The best part is, it will only fetch the state that is actually necessary! It also supports writing/mapping back to the persistence model in an efficient manner. Since it does dirty tracking for you, it will only flush changes if the object is actually dirty.
public Integer merge(final ParentDomainObject parentDomainObject) {
this.entityViewManager.save(this.entityManager, parentDomainObject);
this.entityManager.flush();
return parentDomainObject.getParentId();
}
I am making a Spring Boot backend, and I have the following problem. When I get a Software from VersionableFileRepository and call the getSystem function on that I get the actual System within the relationship. But when I get a Documentation from VersionableFileRepository its getSystem function returns null. I handle the Software and Documentation in the same way, and all instance of these have a System.
Illustrated with code:
versionableFileRepository.findById(fileId).get().getSystem() returns a valid System when fileId identify a Software and returns null when a Documentation
What's wrong? Did I mess something up in the implementation?
I have the following classes:
#Entity
public class System {
#Id
#GeneratedValue
private long id;
private String name;
#OneToOne(cascade = CascadeType.REMOVE, orphanRemoval = true)
#JoinColumn(name = "software_id", referencedColumnName = "id")
private Software software;
#OneToOne(cascade = CascadeType.REMOVE, orphanRemoval = true)
#JoinColumn(name = "documentation_id", referencedColumnName = "id")
private Documentation documentation;
//other fields, getters and setters...
}
#Entity
#Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
public abstract class VersionableFile {
#Id
#GeneratedValue
private long id;
#OneToMany(mappedBy = "file", cascade = CascadeType.REMOVE, orphanRemoval = true)
private List<FileVersion> versions = new ArrayList<>();
public abstract System getSystem();
public abstract void setSystem(System system);
//getters and setters...
}
#Entity
public class Software extends VersionableFile {
#OneToOne(mappedBy = "software")
#JsonIgnore
private System system;
#Override
public System getSystem() {
return system;
}
#Override
public void setSystem(System system) {
this.system = system;
}
}
#Entity
public class Documentation extends VersionableFile {
#OneToOne(mappedBy = "documentation")
#JsonIgnore
private System system;
#Override
public System getSystem() {
return system;
}
#Override
public void setSystem(System system) {
this.system = system;
}
}
#Repository
public interface VersionableFileRepository extends CrudRepository<VersionableFile, Long> {
}
Database:
Everything looks good in the database, this is the system table:
And the corresponding objects can be found in the other two tables (software and documentation). Furthermore the appropriate constraints are also defined.
I think this is a JPA issue, because when I get a System object from SystemRepository (not mentioned here) it has the right software and documentation fields.
Thank you in advance for your help!
Have already commented but looking better I think I found something major here.
Proposal 1
Your Entities structure seems good to me. However you have a major Issue with your java code to retrieve those entities back from database.
versionableFileRepository.findById(fileId).get().getSystem()
fileId as well as documentId are plain Long numbers. How would JPA know if you want to retrieve a Software or a Documentation? This will not work. As you have constructed it, it will have separate tables Documentation and Software and each one of those will have a column Id as primary key.
Make it easier for JPA by using specific repositories
#Repository
public interface SoftwareRepository extends CrudRepository<Software, Long> {
}
Then to retrieve software just use softwareRepository.findById(id).get().getSystem()
And
#Repository
public interface DocumentationRepository extends CrudRepository<Documentation, Long> {
}
Then to retrieve documentation just use documentationRepository.findById(id).get().getSystem()
Proposal 2
If you wish to go along the way you are going then I would consider that the error is specifically on your ids that are generated. You want different tables in your case Documentation and Software to have distinct Ids. Then JPA could distinct from the Id what entity you have.
To achieve that you have to change the strategy of generating Ids
public abstract class VersionableFile {
#Id
#GeneratedValue( strategy = GenerationType.TABLE)
private long id;
....
I am following this map a many-to-many association with extra columns tutorial but wasn't quite successfully.
So I have the following entities...
#Data
#Entity
#Table(name = "PEOPLE")
public class People implements Serializable {
#Id
#SequenceGenerator(name = "people", sequenceName = "people_id_seq", allocationSize = 1)
#GeneratedValue(strategy = GenerationType.AUTO, generator = "people")
private long peopleId;
private String peopleName;
#ToString.Exclude
#EqualsAndHashCode.Exclude
#OneToMany(
mappedBy = "people",
cascade = CascadeType.ALL,
orphanRemoval = true
)
private List<PeopleStats> peopleStats;
public void addStats(Stats stats) {
if (this.peopleStats == null) {
this.peopleStats = new ArrayList<>();
}
PoepleStats pStats = PoepleStats.builder().people(this).stats(stats).build();
this.peopleStats.add(pStats);
}
}
#Data
#Entity
#Table(name = "STATS")
public class Stats implements Serializable {
#Id
#SequenceGenerator(name = "stats", sequenceName = "stats_id_seq", allocationSize = 1)
#GeneratedValue(strategy = GenerationType.AUTO, generator = "stats")
private long statsId;
private String statsName;
private String statsDescription;
}
#Data
#Entity
#Table(name = "PEOPLE_STATS")
public class PeopleStats implements Serializable {
#EmbeddedId
private PeopleStatsId peopleStatsId;
#ManyToOne(fetch = FetchType.LAZY)
#MapsId("peopleId")
private People people;
#ManyToOne(fetch = FetchType.LAZY)
#MapsId("statsId")
private Stats stats;
private long value;
}
#Data
#Embeddable
#EqualsAndHashCode
public class PeopleStatsId implements Serializable {
// Putting #Column(name = "people_id") or not doesn't seem to have any effect
private long peopleId;
// Same goes for this
private long statsId;
}
And then with the following unit test..
#RunWith(SpringRunner.class)
#DataJpaTest
public class PeopleRepositoryTest {
#Autowired
private TestEntityManager entityManager;
#Test
public void testSavePeople() {
// People object created
people.addStats(Stats.builder().statsId(new Long(1)).statsName("a").statsDescription("b").build());
this.entityManager.persistAndFlush(people);
}
}
The table generated by hibernate was as such:
Hibernate: create table people_stats (value bigint not null, people_people_id bigint not null, stats_stats_id bigint not null, primary key (people_people_id, stats_stats_id))
And this is the stacktrace..
javax.persistence.PersistenceException:
org.hibernate.PropertyAccessException: Could not set field value 1
value by reflection : [class
com.sample.shared.entity.PeopleStatsId.peopleId] setter of
com.sample.shared.entity.PeopleStatsId.peopleId at
org.hibernate.internal.ExceptionConverterImpl.convert(ExceptionConverterImpl.java:149)
at
org.hibernate.internal.ExceptionConverterImpl.convert(ExceptionConverterImpl.java:157)
at
org.hibernate.internal.ExceptionConverterImpl.convert
and so on ... 63 more
I came across this similar issue, with solution but not working. After trying the first solution, which is creating a new PeopleStatsId object for the #EmbeddedId, it throws me the same error.
Anyone can guide me along? Thanks.
Update 1: I have uploaded a POC on github.
Update 2:
public void addStats(Stats stats) {
if (this.peopleStats == null) {
this.peopleStats = new ArrayList<>();
}
PeopleStats pStats = PeopleStats.builder().peopleStatsId(new PeopleStatsId()).people(this).stats(stats).build();
this.peopleStats.add(pStats);
}
It is now throwing detached entity error.
Caused by: org.hibernate.PersistentObjectException: detached entity
passed to persist: com.sample.Stats at
org.hibernate.event.internal.DefaultPersistEventListener.onPersist(DefaultPersistEventListener.java:124)
at
org.hibernate.internal.SessionImpl.firePersist(SessionImpl.java:807)
... 68 more
Update 3:
I have changed CascadeType.ALL to MERGE and it seem to solve the problem, but I'm not so sure why though. I even removed the portion in update 2 about .peopleStatsId(new PeopleStatsId()) and it works as well. Now I'm even more puzzled.
In People Class:
#OneToMany(
mappedBy = "people",
cascade = CascadeType.MERGE,
orphanRemoval = true
)
private List<PeopleStats> peopleStats;
public void addStats(Stats stats) {
if (this.peopleStats == null) {
this.peopleStats = new ArrayList<>();
}
PeopleStats pStats = PeopleStats.builder().people(this).stats(stats).build();
this.peopleStats.add(pStats);
}
You need to instatiate peopleStatsId in your PeopleStats class. So change line:
#EmbeddedId
private PeopleStatsId peopleStatsId;
to this:
#EmbeddedId
private PeopleStatsId peopleStatsId = new PeopleStatsId();
Hibernate is trying to set fields of PeopleStatsId but the instance is equal to null so the NullPointerException is thrown.
So after help from #K.Nicholas and research, I think I have managed to resolve the issue and learned new things from it.
#K.Nicholas was right about settings the fields for People and Stats but it wasn't quite clear to me initially. Anyway, it got to me later me.
Basically, all other classes stays pretty much the same except for People class.
#Data
#Entity
#Builder
#Table(name = "PEOPLE")
public class People implements Serializable {
private static final long serialVersionUID = 1L;
#Id
#SequenceGenerator(name = "people", sequenceName = "people_id_seq", allocationSize = 1)
#GeneratedValue(strategy = GenerationType.AUTO, generator = "people")
private long peopleId;
private String peopleName;
#ToString.Exclude
#EqualsAndHashCode.Exclude
#OneToMany(
mappedBy = "people",
cascade = CascadeType.ALL,
orphanRemoval = true
)
private List<PeopleStats> peopleStats;
// Maintain the state of objects association
public void addStats(Stats stats) {
if (this.peopleStats == null) {
this.peopleStats = new ArrayList<>();
}
// Important to ensure that a new instance of PeopleStatsId is being passed into the field
// otherwise, PropertyAccessException will be encountered
PeopleStats pStats = PeopleStats.builder()
.people(this)
.stats(stats)
.peopleStatsId(new PeopleStatsId(this.getPeopleId(), stats.getStatsId()))
.build();
this.peopleStats.add(pStats);
}
}
Take note of the comment in addStats method where I need to pass in a new instance of PeopleStatsId object to initialize the PeopleStatsId object which should have been done so in the first place except that it wasn't. Lesson learnt.
I also mentioned that I met with the issue of detached entity previously. It was because I was trying to set in the Stats id field when it wasn't required.
According to Hibernate Guide,
detached
the entity has an associated identifier, but is no longer associated with a persistence context (usually because the persistence
context was closed or the instance was evicted from the context)
In my post, I was trying to set in Stats to People, then persist it.
#Test
public void testSavePeople() {
// People object created
people.addStats(Stats.builder().statsId(new Long(1)).statsName("a").statsDescription("b").build());
this.entityManager.persistAndFlush(people);
}
The .statsId(new Long(1)) was the problem, because it was considered as a detached entity since there was an id. CascadeType.MERGE would work in this case is because I think due to the saveOrUpdate feature? Anyway, without setting statsId, CascadeType.ALL would work just fine.
An sample of the unit test (working):
#Test
public void testSavePeopleWithOneStats() {
// Creates People entity
People people = this.generatePeopleWithoutId();
// Retrieve existing stats from StatsRepository
Stats stats = this.statsRepository.findById(new Long(1)).get();
// Add Stats to People
people.addStats(stats);
// Persist and retrieve
People p = this.entityManager.persistFlushFind(people);
assertThat(p.getPeopleStats().size()).isEqualTo(1);
}
I had a data-h2.sql script which loaded in Stats data upon starting of unit test, that's why I can retrieve it from statsRepository.
I have also updated my poc in github.
Hope this helps with whoever comes next.
Maybe this is an overly simple question, but I am getting an exception when I try to delete a user entity.
The user entity:
#Entity
#Table(name = "users")
public class User
{
#Transient
private static final int SALT_LENGTH = 32;
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private int id;
#NotNull
private String firstName;
#NotNull
private String lastName;
#Column(unique = true, length = 254)
#NotNull
private String email;
// BCrypt outputs 60 character results.
#Column(length = 60)
private String hashedPassword;
#NotNull
private String salt;
private boolean enabled;
#CreationTimestamp
#Temporal(TemporalType.TIMESTAMP)
#Column(updatable = false)
private Date createdDate;
And I have an entity class which references a user with a foreign key. What I want to happen is that when the user is deleted, any PasswordResetToken objects that reference the user are also deleted. How can I do this?
#Entity
#Table(name = "password_reset_tokens")
public class PasswordResetToken
{
private static final int EXPIRATION_TIME = 1; // In minutes
private static final int RESET_CODE_LENGTH = 10;
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private int id;
private String token;
#OneToOne(targetEntity = User.class, fetch = FetchType.EAGER)
#JoinColumn(nullable = false, name = "userId")
private User user;
private Date expirationDate;
The exception I am getting boils down to Cannot delete or update a parent row: a foreign key constraint fails (`heroku_bc5bfe73a752182`.`password_reset_tokens`, CONSTRAINT `FKk3ndxg5xp6v7wd4gjyusp15gq` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`))
I'd like to avoid adding a reference to PasswordResetToken in the parent entity, becaue User shouldn't need to know anything about PasswordResetToken.
It is not possible on JPA level without creating a bidirectional relation. You need to specify cascade type in User class. User should be owner of the relation and it should provide the information on how to deal with related PasswordResetToken.
But if you cannot have a bidirectional relation I would recommend you to setup relation directly in schema generation SQL script.
If you create your schema via SQL script and not via JPA autogeneration (I believe all serious projects must follow this pattern) you can add ON DELETE CASCADE constraint there.
It will look somehow like this:
CREATE TABLE password_reset_tokens (
-- columns declaration here
user_id INT(11) NOT NULL,
CONSTRAINT FK_PASSWORD_RESET_TOKEN_USER_ID
FOREIGN KEY (user_id) REFERENCES users (id)
ON DELETE CASCADE
);
Here is the documentation on how to use DB migration tools with spring boot. And here is the information on how to generate schema script from hibernate (that will simplify the process of writing your own script).
Parent Entity:
#OneToOne
#JoinColumn(name = "id")
private PasswordResetToken passwordResetToken;
Child Entity:
#OneToOne(mappedBy = "PasswordResetToken", cascade = CascadeType.ALL, orphanRemoval = true)
private User user;
If you want the Password entity to be hidden from the client, you can write a custom responses and hide it. Or if you want to ignore it by using #JsonIgnore
If you don't want the reference in the Parent Entity (User), then you have to override the default method Delete() and write your logic to find and delete the PasswordResetToken first and then the User.
You can use Entity listener and Callback method #PreRemove to delete an associated 'Token' before the 'User'.
#EntityListeners(UserListener.class)
#Entity
public class User {
private String name;
}
#Component
public class UserListener {
private static TokenRepository tokenRepository;
#Autowired
public void setTokenRepository(TokenRepository tokenRepository) {
PersonListener.tokenRepository = tokenRepository;
}
#PreRemove
void preRemove(User user) {
tokenRepository.deleteByUser(user);
}
}
where deleteByPerson is very simple method of your 'Token' repository:
public interface TokenRepository extends JpaRepository<Token, Long> {
void deleteByUser(User user);
}
Pay attention on static declaration of tokenRepository - without this Spring could not inject TokenRepository because, as I can understand, UserListener is instantiated by Hybernate (see additional info here).
Also as we can read in the manual,
a callback method must not invoke EntityManager or Query methods!
But in my simple test all works OK.
Working example and test.
I'm developing a filing system where I have 3 tables. PROJECTS table consists of projectid, project name and other details (see below). This is an existing class and populated schema and I do not want to modify this part of the application if possible.
Folders table (Called ProjectClassification) consists of folderid and foldername and is the owning side of a unidirectional onetomany relationship.
Project_Folders is a join table. I'm using JPA 2.0 (EclipseLink) and JSF 2.0 as my web framework.
My basic problem is I can't add duplicate records to the join table using a merge operation. MERGE is good for adding records until the owning key already exists, after which point it will only update the join table. I know this is the way it's supposed to work but I need to add new records even if there's a duplicate of the owning key. This will allow me to store different projects in the same folder.
I've looked through some other questions here such as:
onetomany unidirectional with jointable setup using jpa
This says what is needed to add one entity to the other in a join table but i need to know more about how to correctly persist or merge the added entity to the database.
The folder entity class:
#Entity
#Table(name = "PROJECTCLASSIFICATIONS")
public class ProjectClassifications implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private int proclassid;
private int projectid;
private String classification;
#OneToMany(cascade = CascadeType.ALL)
#JoinTable(name = "PROJECT_CLASSF_JOIN",
joinColumns = #JoinColumn(name = "proclassid", referencedColumnName = "proclassid"),
inverseJoinColumns = #JoinColumn(name = "projectid", referencedColumnName = "projectid", unique = true))
private Collection<Projects> projects;
public ProjectClassifications() {
}
public ProjectClassifications(String classification) {
this.classification = classification;
}
public ProjectClassifications(int proclassid, int projectid) {
this.proclassid = proclassid;
projects = new ArrayList<Projects>();
}
public ProjectClassifications(Projects newProject) {
projects = new ArrayList<Projects>();
}
public void addProject(Projects newProject) {
if(!getProjects().contains(newProject))
getProjects().add(newProject);
}
....
....
The Project entity class is a pre existing code and I do not want to modify at all if possible:
#Entity
#Table(name = "PROJECTS")
public class Projects {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private int projectid;
private String projectName;
private String projectDescription;
#Temporal(javax.persistence.TemporalType.DATE)
private Date startDate;
#Temporal(javax.persistence.TemporalType.DATE)
private Date endDate;
private String commnts;
// foreign keys to parent tables
private int fk_countryid;
private int fk_companyid;
private int fk_employeeid;
#ManyToOne(optional = true)
#JoinColumn(name = "countryid")
private Country country;
....
....
I then use two html select lists to select values for projectid and proclassid which call the following methoid using a JSF managed bean:
public String makeProClassRecord() {
newProClass = new ProjectClassifications(proclassid, projectid);
newProject = proServ.findByProjectId(projectid);
newProClass.addProject(newProject);
facade.update(newProClass);
//facade.save(newProClass);
return showProclass();
}
My questions are:
1) Is MERGE the correct operation used to add records into a join table?
2) Is there a way to add records that contain duplicate keys (foreign keys represented as new records in the join table) using MERGE?
3) Should PERSIST be used to achieve question 2?
4) Would it be better to create an entity for the join table itself and simply use a PERSIST method to insert the records?
Many thanks
So I solved this myself a couple of weeks ago and thought of sharing the answer. Instead of doing merge or persist operations on any of the target entities, I created a Join table and unidirectional OneToMany relationship from the Project entity to the below ProjectFileSystem join table entity and simply did the persist operation using that entity. I need to add duplicate folders for different projects (or store many projects under a single folder item) so it seems more efficient to do the CRUD operations in the actual join table entity rather than from the target entity. Hope this helps:
#Entity
#Table(name = "PROFOLDERSYS_JOIN")
public class ProjectFileSystem implements Serializable{
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private int foldersysid;
private int proclassid;
private int projectid;
private String projectName;
private String folderName;
public ProjectFileSystem() {
}
public ProjectFileSystem(int proclassid, int projectid,
String projectName, String folderName) {
this.proclassid = proclassid;
this.projectid = projectid;
this.projectName = projectName;
this.folderName = folderName;
}
// getters and setters
}
The method in the bean would be:
public String makeProSys() {
newProSys = new ProjectFileSystem(proclassid, projectid, classification, projectName);
newProject = proServ.findByProjectId(projectid);
projectName = newProject.getProjectName();
newProSys.setProjectName(projectName);
newProClass = facade.findByContactId(proclassid);
classification = newProClass.getClassification();
newProSys.setFolderName(classification);
profilFacade.save(newProSys);
return showProSys();
}