Skip to content

Latest commit

 

History

History
146 lines (111 loc) · 4.43 KB

File metadata and controls

146 lines (111 loc) · 4.43 KB

Clean Code Rules for Spring Data / JPA

Entity Validation via JSR 303 annotations

While you may use @jakarta.persistence.Column to influence constraints on the database like Nullability or maximum length of column, it is better to use JSR 303 annotations here, as Hibernate checks this already in its validator implementation before attempting to insert to the database. You also get better validation errors then at runtime. See this article for more information.

A valid entity configuration would be something like this.

@Entity
class Project(

    @field:NotNull
    @field:Size(max = 30)
    val userId: String,

    @field:NotNull
    var lastModifiedDate: OffsetDateTime = OffsetDateTime.now(),
) {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Long = 0

    @NotNull
    val createdDate: OffsetDateTime = OffsetDateTime.now()

    var orderedDate: OffsetDateTime? = null
}

See more details in the next chapters:


Use @NotNull instead of @Column(nullable=false)

Rule-ID: jpa.entities-not-null-instead-of-nullable

Use @jakarta.validation.NotNull instead of @Column(nullable=false) to express that a column must not be null.

Be aware that if your field is used inside a Kotlin constructor, you need to prefix the annotation with @field: as listed here:

@Entity
class Project(
    @Id
    val projectId: String,

    @field:NotNull
    val userId: String
) {}

Use @Size instead of @Column(length=...)

Rule-ID: jpa.entities-size-instead-of-length

Use @jakarta.validation.Size(max = 30) instead of @Column(length=30) to express the maximum length of a column.

Be aware that if your field is used inside a Kotlin constructor, you need to prefix the annotation with @field: as listed here:

@Entity
class Project(
    @Id
    val projectId: String,

    @field:NotNull
    @field:Size(max = 30)
    val userId: String
) {}

Don't use explicit column names

Rule-ID: jpa.entities-no-column-names

Avoid using explicit column names by using @Column(name="my_column_name"), use the defaults from our appropriate NamingStrategy instead. Exceptions might be that you need to access a legacy database where you have no control over the database scheme.


Kotlin nullability should match JPA nullability

Rule-ID: jpa.nullable-flag-of-kotlin-needs-to-match-jpa-specification

In case you are using Kotlin, you should also have the JSR 303 annotations in sync with Kotlin nullability feature:

Consider the following code:

@Entity
class MyEntity {
    // other columns including @Id have been suppressed here

    var name: String
    var country: String?
}

As you see here, the field country is nullable in Kotlin, whereas the field 'name' is not. Unfortunately, this null-check is not reflected to the database, that is why you should add that explicitely with a @jakarta.validation.constraints.NotNull annotation as follows:

@Entity
class MyEntity {
// other columns including @Id have been suppressed here

    @NotNull
    var name: String
    var country: String?
}

That way, also database generation (if activated) will mark that column as NON NULL in the database.

Additional Considerations for @Id columns in Kotlin

From a database perspective a primary key column serving always has a NOT NULL CONSTRAINT associated with it. Accordingly, the @Id annotated property should be a non nullable val. But now hibernate generated keys feel problematic, because we will have to construct an entity including the non nullable id before calling JpaRepository.save(), hence before hibernate can generate a key.

This can be alleviated though. Consider the following example:

@Entity
class MyEntity(
    var name: String
){
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Id
    val id: Long = 0
}

When MyEntity is constructed it will have myEntity.identifier == 0, after a call to JpaRepository.save() a new ID will be generated. After a call to JpaRepository.save() the object supplied as argument must not be used anymore, instead the returned Entity, which has the generated ID, must be used.