Pablo Fernandez
Pablo Fernandez

Reputation: 287400

What's the proper way of generating a unique field using Spring Boot / JPA?

First of all, I'm not taking about the primary id of the record. I'm talking about an field that is used by users to identify the record that's automatically generated but changeable by the user, not sequential and not a UUID. For example, starting with an account entity:

@Entity
@Data
class Account {
    @Id
    @GeneratedValue
    private int id;

    @Column(unique=true)
    @NotNull
    private String slug;

    @Column
    private String name;
}

and then I simply create a record:

@Autowired
private AccountRepository accountRepository;

Account account = new Account();
account.setName("ACME");
accountRepository.saveAndFlush(account);

At that point, the slug should have been generated, either completely randomly, or by doing something based on the name. How should that be done?

I know without locking the whole table it's impossible to ensure that the insertion won't result in an exception due to the uniqueness constrain being violated. I'm actually OK blocking the whole table or even letting the exception happen (you need a lot of requests per second fora conflict to happen between the check for availability and the insert).

Upvotes: 1

Views: 4482

Answers (3)

Motolola
Motolola

Reputation: 368

I will do something like a separate Bean, helper or service class like this.

public class SlugService {
    public String generateSlug(String slug)
    {                                                   
      if (accountRepo.getBySlug(slug) != null){ //check if it is already
        return slug
      } else {
       slug.append("-"); //whatever the syntax
       generateSlug();
      }
    }

    public String makeSlug()
    {
      String slug = split by " ", replace by "_"(accountObject.getName);
      generateSlug(slug)
    }   
}

Call the makeSlug(); method.

Upvotes: 0

Kayaman
Kayaman

Reputation: 73528

If you separate the slug from the Account table and put it in a (id, slug) table by itself, you can generate the slug first (retrying until you succeed) and then persist the Account with a link to the just generated slug id.

You can't achieve this in a @PrePersist method, so your service needs to create the slug whenever you're creating an new Account. However it does simplify things on the application side (e.g. you don't need to wonder which constraint was violated when persisting an Account).

Depending on your other code, you can also get around locking the Account table and even the Slug table if you go for the optimistic approach.

A pseudo-code example of a service method that creates a new account (providing new Slug() creates the random slug):

@Autowired SlugRepository slugRepository;
@Autowired AccountRepository accountRepository;

public void createAccount(Account a) {
    Slug s = null;
    while(s == null) {
        try {
            s = slugRepository.save(new Slug());
        } catch(Exception e) {
        }
    }
    a.setSlug(s);
    accountRepository.save(a);
}

Upvotes: 1

Rohit
Rohit

Reputation: 2152

I can think of JPA callbacks to generate the slug. In your case @PrePersist could be useful.

That said, why you need to make sure the value is available with a select before inserting the record, so the window for a collision to occur is tiny? You do have unique constraint on the column, right?

Update

Personally I would prefer to address it like this:

  1. Use JPA callback @PrePersist when generating the the slug. Use to random UUID or timestamp to minimise the possibility of collision. No checking for collision as chances are minimal.
  2. When updating the Account for user generated slug, always check first using query for collision. This check will offcourse happen in service update method itself.

This way I can be DB agnostic and also don't have to use repository/service in entity or listener classes.

Upvotes: 0

Related Questions