Search code examples
validationspring-data-jpa

Validations work when using Long but don't work when using UUID


I'm using Java and Spring data jpa. Up until now I was using Long as my Id. But it turned out I needed it in my DTOs. So for security reasons I need UUID or GUID. So I read a little about it and changed to UUID from Long. Expected it to be smooth and not have many problems switching but here we are.

So I have a Entity, Repository, DTO, Mapper(generated by mapstruct). The reason I'm listing all of them in case you have advice on how I've structured my code.

I have a test in place and that's testing the repository.

That's the test

//I'm using this on top of my test class
@DataJpaTest
public class RoleRepositoryTests {
@Test
    public void saveToDBShouldThrowExceptionTest(){
        //Given
        Role userRole = new Role();

        try {
            //When
            roleRepository.save(userRole);

            //Then
            fail("Expected ConstraintViolationException was not thrown");
        } catch (ConstraintViolationException e) {
            //The above will throw two exceptions cuz
            //both @NotBlank and @NotNull will throw an exception.
            //it throws them in random order every time
            assertThat(e.getConstraintViolations().size()).isEqualTo(2);
            return;
        }
        fail("Different Exception was thrown");
    }
}

The entity

@Entity
@Getter @Setter
@ToString
@NoArgsConstructor
@Table(name = "Roles")
public class Role {

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

    @NotNull
    @NotBlank
    private String name;

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Role role = (Role) o;
        return id.equals(role.id) && name.equals(role.name);
    }

    @Override
    public int hashCode() {
        return Objects.hash(id, name);
    }
}

The repository

@Repository
public interface RoleRepository extends CrudRepository<Role, UUID> {
    Optional<Role> findByName(String name);
}

In the test I expect roleRepository.save(userRole) to throw a ConstraintViolationException. It doesn't do it for some reason. Since I switched from Long to UUID I decided to go back and change back stuff to Long and see if that works(of course I changed the repository, the generation type and the field type). The baffling thing is it worked. Test threw the exceptions so the validations worked. I have no idea what's causing this.

@DataJpaTest creates a h2 in memory db by default. I haven't changed that and that's what I'm testing with. The tables, constraints and not nulls are created as expected.

create table Roles (
       id uuid not null,
        name varchar(255) not null,
        primary key (id)
    )

It manages to save a name that is null to the above table somehow although the not null is there.

I tried testing against my real db (ms sql) Roles Table. No exceptions thrown but nothing was saved to the db as well. Used this annotation @AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) to test against the real db.

it generates a different UUID every time. (Idk if it should be doing that or not)

This happens with all entites that I have. So I have no idea what I am missing.

I've tried clean building with maven. I've tried adding @Validated to both my repository and entity. I've tried restarting my IDE (IntelliJ) I'll try to restart my PC after I post this. Didn't work. Tried a few more things and got to a unrecognized id type : uuid -> java.util.UUID. Got it by doing @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private UUID id;

Found this post. Currently reading into it.


Solution

  • Found a solution! You don't let the database generate the GUID/UUID. You generate it in the code.

    @Entity
    @Getter @Setter
    @ToString
    @Table(name = "Roles")
    public class Role {
    
        //Make this String and remove the @GeneratedValue annotation
        @Id
        private String id;
    
        @NotNull
        @NotBlank
        private String name;
    
        //If an empty entity is created generate a UUID. (You can still overwrite it later)
        public Role() {
            this.id = UUID.randomUUID().toString();
        }
    
        public Role(String id, String name) {
            this.id = id;
            this.name = name;
        }
    
    }
    

    Also don't forget to update the repository to String!

    Also it still doesn't straight up throw a ConstraintViolationException.

    This works but when you try to save something that violates the validations it throws a org.springframework.transaction.TransactionSystemException: Could not commit JPA transaction which is caused by Caused by: jakarta.validation.ConstraintViolationException: Validation failed for classes. I'd expect it to straight up throw ConstraintViolationException but here we are.

    Still no validations using h2. So basically I didn't solve shit.


    Old semi-solution:

    Kinda found a solution but it isn't exactly what I expected/wanted.

    So the thing that causes this issue is the db (h2 or mssql) and spring data jpa(hibernate) can't agree on what to do. The closest I got to a solution is

    @Id
    @GenericGenerator(name = "uuid", strategy = "org.hibernate.id.UUIDGenerator")
    @GeneratedValue(generator = "uuid")
    private UUID id;
    

    or

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

    or when using Hibernate 6+

    @Id
    @UuidGenerator(style = UuidGenerator.Style.RANDOM)
    //you can add this but it will just be ignored    
    //@GeneratedValue(strategy = GenerationType.IDENTITY)
    private UUID id;
    

    This works but when you try to save something that violates the validations it throws a org.springframework.transaction.TransactionSystemException: Could not commit JPA transaction which is caused by Caused by: jakarta.validation.ConstraintViolationException: Validation failed for classes. I'd expect it to straight up throw ConstraintViolationException but here we are.

    Also this doesn't work with h2. h2 doesn't respect the validations for some reason. It just straight up ignores table and entity validations.

    The solution that should've worked out of the box but it won't with mssql nor h2 is

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

    H2 nicely(while throwing a few exceptions and not creating the tables) tells me it doesn't support it.

    MSSQL tells me it doesn't know what uuid is. org.springframework.orm.jpa.JpaSystemException: unrecognized id type : uuid -> java.util.UUID. Tried changing the GenericGenerator strategy to guid as well. Got the idea from here. (guid - uses a database-generated GUID string on MS SQL Server and MySQL.)

    If you somehow manage to get that working with .IDENTITY I think you'll need either this (newsequentialid()) or (newId()) as default value in mssql.

    I'm sure there is a better solution so I won't mark this as an answer. The better solution is just above ^.