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!