10

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.

Bassinator
  • 1,682
  • 3
  • 23
  • 50
  • 1
    suppose you take a look at this [post](https://vladmihalcea.com/2015/03/05/a-beginners-guide-to-jpa-and-hibernate-cascade-types/) and it explains more about your issue and solution. – Rajith Pemabandu May 25 '17 at 00:42
  • suppose you have to add `@OneToOne(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true)` – Rajith Pemabandu May 25 '17 at 00:49
  • To the user entity? Or the PasswordResetToken entity? I didn't really want to add any reference to the child in the User entity, since the user entity shouldn't need to know about the existence of the reset token. – Bassinator May 25 '17 at 00:51
  • to `PasswordResetToken` – Rajith Pemabandu May 25 '17 at 00:58
  • I get `Unknown MappedBy` error. Why? – Bassinator May 25 '17 at 01:08
  • If you do not want to update the JPA model, you will need to cascade the delete on the database side as `ALTER TABLE password_reset_tokens DROP CONSTRAINT FKk3ndxg5xp6v7wd4gjyusp15gq;`, followed by `ALTER TABLE password_reset_tokens ADD CONSTRAINT FKk3ndxg5xp6v7wd4gjyusp15gq FOREIGN KEY (userId) REFERENCES users(id) ON UPDATE CASCADE ON DELETE CASCADE;` – manish May 25 '17 at 03:32

3 Answers3

10

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).

Sasha Shpota
  • 9,436
  • 14
  • 75
  • 148
  • 1
    I agree you need it in the DDL - then future developers are left trying to decipher how the child record has disappeared before checking the DDL – farrellmr Jun 02 '17 at 09:32
5

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.

zeagord
  • 2,257
  • 3
  • 17
  • 24
2

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.

Cepr0
  • 28,144
  • 8
  • 75
  • 101