Spring + JpaRepository + Generated key: instruct key not null when loading from database


#1

Consider the following entity:

@Entity
@Table(name = "test_table")
class Test {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    var testIdPk: Long? = null

    var number: Int = 0
}

for it to work with JpaRepository, testIdPk must be nullable - otherwise, I won’t be able to save new entities.

On the other hand, anytime such entity is loaded from the database, we know testIdPk is not null.

Right now anytime I want to access testIdPk I need to !! it.

I was wondering if there is a way to mark JpaRepository methods, to tell kotlin compiler that Test objects coming from these marked methods would have non-null testIdPk. I’ve looked at contracts, but it won’t work with interfaces.


#2

I suggest 2 approaches to that:

@Entity
@Table(name = "test_table")
class Test {
	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	val testIdPk: Long = 0
	
	var number: Int = 0
}

or:

@Entity
@Table(name = "test_table")
class Test(
	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	val testIdPk: Long = 0,
	
	var number: Int = 0
)

Entity with id 0 is interpreted as one without identity.


#3

As @madmax1028 says, for simple keys, I haven’t found a better wya than supplying a dummy value (e.g. 0, -1, or "").

For other fields, especially for references to other entities, you could instead use lateinit.

Neither approach is elegant, but seem to both work well enough in practice. (For the first, there’s often a natural value to use, but in general it doesn’t matter as your code will never see it when loading existing objects; and you wouldn’t bother setting it when creating new ones.)


#4

Thanks a lot. Somehow I was convinced that it needs to be null.

Some info on why does this work if someone else lands here:

Looking into org.springframework.data.repository.core.support.AbstractEntityInformation

there’s a method used to determine if the entity is new:

	public boolean isNew(T entity) {

		ID id = getId(entity);
		Class<ID> idType = getIdType();

		if (!idType.isPrimitive()) {
			return id == null;
		}

		if (id instanceof Number) {
			return ((Number) id).longValue() == 0L;
		}

		throw new IllegalArgumentException(String.format("Unsupported primitive id type %s!", idType));
	}

So if I understand this correctly, in Kotlin:
val testIdPk: Long = 0 would work, since here Long would be converted to primitive long,

but:
val testIdPk: Long? = 0 would fail, since the first check id == null would be in effect (so it would actually save the entity with id set to 0).