1

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

Pablo Fernandez
  • 279,434
  • 135
  • 377
  • 622
  • Have you tried javax.persistence.GeneratedValue? – Alain-Michel Chomnoue N Sep 07 '17 at 11:01
  • @chomnoue: no, as far as I can see that's for primary keys and doesn't have a strategy that allows you generate arbitrary values. Am I wrong? – Pablo Fernandez Sep 07 '17 at 11:02
  • 1
    What about java.util.UUID#randomUUID? – Alain-Michel Chomnoue N Sep 07 '17 at 11:06
  • @chomnoue what I'm trying to generate is a short, random and/or concise and changeable ID. An UUID is nothing of those things. – Pablo Fernandez Sep 07 '17 at 11:07
  • @Kayaman because it's convenient for the users to type an id that's easy to remember rather than a random one, so, if they want to modify it, I want to let them. – Pablo Fernandez Sep 07 '17 at 11:26
  • Which database are you using (or do you intend this to be database agnostic)? It can still be a good idea to let the database generate it. Also, what kind of row count are you expecting? If you didn't get collisions with 5 char alphanumeric, then you must be working with quite a small dataset? – Kayaman Sep 07 '17 at 11:41
  • @Kayaman: I'm using PostgreSQL, I prefer to be database agnostic, but I'll evaluate alternatives. I have a table with a couple millions of records. Values are generated that already exist, but in that case, I increase the size of the slug and generate a new one. The only problematic collision is when two records get the same random slug at the same time and that hasn't happened. – Pablo Fernandez Sep 07 '17 at 11:44
  • I'd say you have two basic possibilities. Let the database handle the generation with a trigger, or do it yourself (for example in `@PrePersist`). Having the database generate it would be the path of least resistance, since even though the syntax may differ, all of them have a way to calculate an unused slug for the row. Then again it's not that hard to do it on the application side either, you just have to get a big enough slug that collisions aren't too common (and with 2 million rows, the birthday paradox rears its ugly head), and handle them gracefully. – Kayaman Sep 07 '17 at 11:57
  • @Kayaman: I'd rather do it in code, but PrePersist doesn't have access to the database to check for collisions to mitigate them: https://stackoverflow.com/questions/46092710/how-can-i-access-the-repository-from-the-entity-in-spring-boot/46094953#46094953 – Pablo Fernandez Sep 07 '17 at 12:00
  • I know. Java doesn't have the equivalent of ActiveRecord. That's why you need to handle the collisions when they occur. It'll be an optimistic "not-locking" strategy. – Kayaman Sep 07 '17 at 12:03
  • @Kayaman: ok, but then PrePersist isn't the whole solution, only a tiny trivial part. I don't believe it can be done because last time I checked, at least PostgreSQL, doesn't tell you which column conflicted, but I might be wrong. Still, where should that code be? – Pablo Fernandez Sep 07 '17 at 12:04
  • Well, you *could* get the constraint name from the exception, but it would be quite a hackish approach. – Kayaman Sep 07 '17 at 12:10
  • Since JPA isn't as "married" to the database as active record, it's quite difficult to achieve this in application (well JPA) code only. I'd go for the trigger option as the cleanest choice. – Kayaman Sep 07 '17 at 12:27
  • I'm dumbfounded by how hard this is with JPA. – Pablo Fernandez Sep 07 '17 at 12:41
  • 1
    Why don't you override the `save()` of the `AccountRepository` and apply the logic for the generation of your unique attribute there? Why is there a need to do it in callback? – Eirini Graonidou Sep 07 '17 at 12:45
  • @EiriniGraonidou: because it's an interface, not a class. Also, I think it could be saved without using that specific repo (through an association for example). Am I wrong? – Pablo Fernandez Sep 07 '17 at 12:47
  • Don't be dumbfounded. `JPA` and `Active Record` are two different patterns. If you're used to one, you tend to try to port it to another. That's how people end up writing programming languages as if they were other programming languages, and eventually you get to [If you want X you know where to get it](http://catb.org/jargon/html/I/If-you-want-X--you-know-where-to-find-it-.html). – Kayaman Sep 07 '17 at 12:49
  • @Kayaman: I'm not trying to do something Active Record specific by trying to generate a unique value mitigating collisions. I'm happy to do it the JPA way, but it seems tho JPA way is that it's impossible. – Pablo Fernandez Sep 07 '17 at 12:55
  • 1
    @Pablo you could take a look here: https://stackoverflow.com/questions/13036159/spring-data-override-save-method. I think you should decide if you want to use a Repository or a DAO pattern for your data access layer. – Eirini Graonidou Sep 07 '17 at 12:56
  • 2
    You're not trying to do anything Active Record *specific*, but you're thinking like you're working with Active Record. You're surprised that it's harder than with Active Record. You're comparing what you've done and released as a library, with something you're about to create with a different technology with different design ideas. – Kayaman Sep 07 '17 at 13:21
  • 1
    Where it gets shaky is comparing an implementation (ActiveRecord) VS an API (JPA). Any of the implementations of JPA - Hibernate, EclipseLink, OpenJPA, may indeed provide functionality that you're after; the JPA specification only concerns itself with the ORM aspect that all the implementations need to provide in a compatible way. You'd have to check their documentation to see what's truly available. – Gimby Sep 07 '17 at 13:28
  • I'm happy to use anything from Hibernate, I don't need to limit myself to JPA. – Pablo Fernandez Sep 07 '17 at 22:49
  • Even raw Hibernate doesn't have the tools for this. You'd have to go to down all the way to JDBC. This is an interesting question though, highlighting some differences between AD and JPA, and how JPA is actually quite detached from the DB. – Kayaman Sep 08 '17 at 06:29

3 Answers3

1

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);
}
Kayaman
  • 72,141
  • 5
  • 83
  • 121
  • I think this adds a lot of complexity (another table) when none is needed. But, how would the Account entity create the Slug entries anyway? – Pablo Fernandez Sep 07 '17 at 12:56
  • 2
    Adding a table doesn't create complexity. It creates beautiful and efficient normalization (with the perks I mentioned in the answer). Entities also don't create other entities. Service methods create entities, so you'd have a nice `AccountService.createAccount()` that would then create the slug as well. The CRUD repository approach is for simple operations only. If you need more complicated logic, you need a proper service method (in which you can of course use `SlugRepository` and `AccountRepository`) – Kayaman Sep 07 '17 at 13:00
  • I respectfully disagree. Semantically, slug is a field of account. An extra table adds complexity. Regardless, whichever class would create that Slug object, could as well just do the same for creating the account, so, there's no need for a separate table at all. – Pablo Fernandez Sep 07 '17 at 22:52
  • @Pablo Semantically slug is part of account, yes. How you store it is an entirely different beast. DBAs would beat you over the head if you started to explain how they've "designed their relational database wrong" because your application model isn't identical. I'm assuming you're more used to dealing with the db through the programming language (i.e. rails)? I think the problem here is that you're looking at this from your viewpoint, hoping you won't have to deviate too much from what you're used to. This is a decent answer without hacks. If there were a better one (with JPA), I'd tell you. – Kayaman Sep 08 '17 at 05:17
0

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.

Rohit
  • 2,132
  • 1
  • 15
  • 24
  • yes, to mitigate collisions. Collisions are likely to occur and if I let the exception happen, it's much harder to handle. Once I mitigate the collision, they become so rare that I never seen it happen once in a system with a couple hundred thousand users. – Pablo Fernandez Sep 07 '17 at 12:12
0

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.

Motolola
  • 368
  • 5
  • 19