What I find a better way, is using a dedicated object for the primary key of each entity. So we have classes like UserId and OrderId.
To do this, we first will create an Entity interface:
public interface Entity<T extends EntityId> {
T getId();
}
This uses the EntityId interface that represents the primary key object:
import java.io.Serializable;
/**
* Interface for primary keys of entities. * * @param <T> the underlying type of the entity id
*/
public interface EntityId<T> extends Serializable {
T getValue();
String asString();
}
This interface will "hide" the fact that a long is used, but it is generic so any underlying type can be used (E.g. a UUID is also possible).
Using these classes, our User entity becomes:
@javax.persistence.Entity
public class User implements Entity<UserId> {
@Id
@GeneratedValue // Will not work!
private UserId id;
...
}
Now, this will not work out of the box since Hibernate will not know how to create a UserId object. To make it work, we need to create our own IdentifierGenerator to bridge the long that is generated from the database with our own UserId object.
public class UserId {
private Long value;
public UserId(Long value) {
this.value = value;
}
public Long getValue() {
return value;
}
public String asString() {
return String.valueOf(value);
}
}
Next the UserIdIdentifierGenerator:
public class UserIdIdentifierGenerator implements IdentifierGenerator, Configurable {
private String sequenceCallSyntax;
@Override
public void configure(Type type, Properties params, ServiceRegistry serviceRegistry) throws MappingException {
JdbcEnvironment jdbcEnvironment = serviceRegistry.getService(JdbcEnvironment.class);
Dialect dialect = jdbcEnvironment.getDialect();
final String sequencePerEntitySuffix = ConfigurationHelper.getString(SequenceStyleGenerator.CONFIG_SEQUENCE_PER_ENTITY_SUFFIX, params, SequenceStyleGenerator.DEF_SEQUENCE_SUFFIX);
boolean preferSequencePerEntity = ConfigurationHelper.getBoolean(SequenceStyleGenerator.CONFIG_PREFER_SEQUENCE_PER_ENTITY, params, false);
final String defaultSequenceName = preferSequencePerEntity ? params.getProperty(JPA_ENTITY_NAME) + sequencePerEntitySuffix : SequenceStyleGenerator.DEF_SEQUENCE_NAME;
sequenceCallSyntax = dialect.getSequenceNextValString(ConfigurationHelper.getString(SequenceStyleGenerator.SEQUENCE_PARAM, params, defaultSequenceName));
}
@Override
public Serializable generate(SharedSessionContractImplementor session, Object obj) throws HibernateException {
if (obj instanceof Entity) {
Entity entity = (Entity) obj;
EntityId id = entity.getId();
if (id != null) {
return id;
}
}
long seqValue = ((Number) ((Session) session).createNativeQuery(sequenceCallSyntax).uniqueResult()).longValue();
return new UserId(seqValue);
}
}
The most important part is the generate method. It will get a new unique long from the database, which we then use to create the UserId object. Hibernate will set this object on our User object.
We can now use the UserIdIdentifierGenerator in our User entity:
@javax.persistence.Entity
public class User implements Entity<UserId> {
@EmbeddedId
@GenericGenerator(name = "assigned-sequence", strategy = "com.wimdeblauwe.examples.primarykeyobject.user.UserIdIdentifierGenerator")
@GeneratedValue(generator = "assigned-sequence", strategy = GenerationType.SEQUENCE)
private UserId id;
Note that we need to use @EmbeddedId instead of @Id.
Finally, adjust UserRepository to indicate that the UserId type is now used:
public interface UserRepository extends CrudRepository<User, UserId> {}
This can be validated with this @DataJpaTest test:
@DataJpaTest
class UserRepositoryTest {
@Autowired
private UserRepository repository;
@Test
@Sql(statements = "CREATE SEQUENCE HIBERNATE_SEQUENCE")
public void testSaveUser() {
User user = repository.save(new User("Wim"));
assertThat(user).isNotNull();
assertThat(user.getId()).isNotNull().isInstanceOf(UserId.class);
assertThat(user.getId().getValue()).isPositive();
}
}
The sequence table is here created in the unit test itself. In an actual application, you should use Flyway (or Liquibase) to do proper database initialization and migrations.
Our service interface now becomes:
public interface OrderService {
Order getOrder(OrderId orderId, UserId userId);
}
So now there is no way to accidentally pass a UserId in an OrderId parameter!