0

I have a spring project and want to enforce uniqueness in the database on a field and get the error message back to the UI.

I have read this SO answer and it makes sense so @Column(unique = true) makes the constraint on the table but doesn't enforce it.

So the question becomes how to create a @Unique annotation that checks with the database and returns a error message into BindingResult on POST handler.

An example would be great.

UPDATE

I tried the following way to make a custom validator:

The objects (note I have added @valid to get the validator messages to navigate up to BindingResult)

Person.java

@Entity
public class Person {
public Person() {}

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
// other stuff
@UniqueNid
private BigInteger nid;

EpisodePerson.java

@Entity
public class EpisodePerson {
public EpisodePerson(){};

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@ManyToOne
@Valid
private Person person;

EpisodeViewModel (DTO)

public class EpisodeViewModel {

@Valid
private Episode episode = new Episode();
@Valid
private List<EpisodePerson> persons = new ArrayList<>();

UniqueNid.java

@Documented
@Target({ElementType.METHOD, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = UniqueNiaValidator.class)
public @interface UniqueNid {

String message() default "{Duplicate ID}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}

UniqueNidValidator.java

public class UniqueNidValidator implements ConstraintValidator<UniqueNid, BigInteger> {
public UniqueNidValidator(){};

private PersonRepository personRepository;

@Autowired
public void setPersonRepository(PersonRepository personRepository) {this.personRepository = personRepository;}

public UniqueNidValidator(PersonRepository personRepository) {
    this.personRepository = personRepository;
}

@Override
public void initialize(UniqueNid constraint) {
}

@Override
public boolean isValid(BigInteger nid, ConstraintValidatorContext context) {
    return nid != null && personRepository.existsByNid(nid);
}
}

PersonRepository.java

...
    Boolean existsByNid(BigInteger nid);
...

Application.java

@SpringBootApplication
@EnableAutoConfiguration(exclude = { org.springframework.boot.autoconfigure.security.SecurityAutoConfiguration.class })

public class Demo3Application extends SpringBootServletInitializer {

@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
    return application.sources(WebApplicationInitializer.class);
}

public static void main(String[] args) {
    SpringApplication.run(Demo3Application.class, args);
}

@Bean
public javax.validation.Validator localValidatorFactoryBean() {
    return new LocalValidatorFactoryBean();
}
}

When I go to submit a person I get :

Stack Trace (abbreviated)

java.lang.NullPointerException: null
at com.example.validators.UniqueNidValidator.isValid(UniqueNidValidator.java:31) ~[main/:na]

UPDATE 2

I have also tried this configuration

public class UniqueNidValidator implements ConstraintValidator<UniqueNid, BigInteger> {

    public UniqueNidValidator(){};

    private PersonRepository personRepository;

    public UniqueNidValidator(PersonRepository personRepository) {
        this.personRepository = personRepository;
    }

    @Override
    public void initialize(UniqueNid constraint) {
    }

    @Override
    public boolean isValid(BigInteger nid, ConstraintValidatorContext context) {
        System.out.println("About to check " +nid.toString());
        System.out.println("person repo " +personRepository.toString() );
        return personRepository.existsByNid(nid);
    }
}

which gives:

java.lang.NullPointerException: null
at com.example.validators.UniqueNiaValidator.isValid(UniqueNiaValidator.java:29) ~[main/:na]

When I try to print the repo to console.

Doug Greaney
  • 181
  • 3
  • 17

3 Answers3

0

You'll need to create a custom validation that checks the database. For the database check you can obviously use the probably already existing Spring Data Repository and it's exists() method.

A custom validation consists of an annotation to mark the fields to be checked and a class implementing the actual check.

On minor challenge is that the class needs a default constructor and doesn't really support injecting dependencies. So anything you need, you have to basically access from some static reference, including e.g. the repository. So you probably have a separate bean which puts the repository into that static reference.

Such a bean that "catches" a repository and makes it available in a static variable might look like this.

@Component
public class RepositoryCatcher{
    public static MyRepository;
    public RepositoryCatcher(MyRepository r){
        repository = r;
    }
}
Jens Schauder
  • 77,657
  • 34
  • 181
  • 348
0

From the exception you mentioned it seems that the only possible NullPointerException is when the personRepository is incorrectly injected to the validator.

Please give a try to the solution below:

  1. Remove the following bean from your Demo3Application and let Spring Boot create the default one instead.

    @Bean
    public javax.validation.Validator localValidatorFactoryBean() {
        return new LocalValidatorFactoryBean();
    }
    
  2. Remove the setter for the repository from the validator but leave the dependency in the constructor as it is right now.

    @Autowired
    public void setPersonRepository(PersonRepository personRepository {
        this.personRepository = personRepository;
    }
    

It's not entirely true that custom validators require a default constructor as mentioned by Jens in his answer. Spring will inject dependencies based on the constructor even though a validator isn't mark as a managed component. The @Autowired annotation is also redundant.

In addition, you probably made a mistake in the condition. You should check if a person doesn't exist (Notice the ! mark in the second part).

return nid != null && !personRepository.existsByNid(nid);

I encourage you to look into a blog post which addresses your issue. Sample code is available in the GitHub repository. You can run, test it, and then compare with your solution.

Daniel Olszewski
  • 13,995
  • 6
  • 58
  • 65
0

This is the validator that worked and errors into BindingResult:

UniqueNidValidator.java

public class UniqueNiaValidator implements ConstraintValidator<UniqueNid, BigInteger> {

public UniqueNiaValidator(){};

@Autowired
private PersonRepository personRepository;

public UniqueNiaValidator(PersonRepository personRepository) {
    this.personRepository = personRepository;
}

@Override
public void initialize(UniqueNid constraint) {
}

@Override
public boolean isValid(BigInteger nid, ConstraintValidatorContext context) {
    return !personRepository.existsByNid(nid);
}
}

Note the !personRepository.existByNid(nid);

Further more the reason that the repo was blank the second time around was because it was getting called twice as outlined here

But checking for RDBMS constraint violations on Beans probably isn't a good idea anyway.

Community
  • 1
  • 1
Doug Greaney
  • 181
  • 3
  • 17