Aggreagate persistence: Persist data
Implement Commands
Command base
-
For each root entity there will be an abstract class Command Base generated in the SDK.
-
The Command Base provides access to the repository, the entity builder, the event builder and the event producer.
-
The Command Base contains abstract methods, one method for each modelled command.
-
These Commands method needs to be implemented in the generated implementation file for the Root Entity Commands.
Command inheritance
Command inheritance is supported, Root Entity
will inherit its parent(s) commands.
-
By default, if your root entity inherits commands from parent entities, By default the generated abstract command class will call these parent command(s) directly.
-
In the command generated stub, user would have the possibility to either
- Override the implementation of these command(s) that are coming from its parent(s).
- Don't override the implementation by deleting that command stub method / calling the parent implementation via super key word.
Factory commands
The main purpose of a factory command is to set values to the properties of a root entity and then persist those changes to the database, thus, creating a new instance.
-
Factory commands will return Root Entity as a return type
-
Factory commands take an Input Entity as the only parameter, if they have been modelled with one.
Example
The factory command to create an instance of a root entity called MasterCard is the following:
@Service
public class MasterCardCommand extends MasterCardCommandBase {
@Override
public void createCreditCard(MasterCard instance, CreateCreditCardInput createCreditCardInput) {
// Fill instance with values from input entity
instance.setBankName(createCreditCardInput.getBankName());
// save entity to repository
repo.cc.masterCard.save(instance);
}
}
Instance commands
Instance commands usually hold logic to modify the state of a root entity instance and persist the updated state in the database. The instance that can be modified as well as the input of the command is provided automatically.
-
Instance commands take an instance of Root Entity as the first parameter named instance and an Input Entity if they have been modelled with one.
-
Instance commands always return void.
-
Instance commands operate against the root entity instance that is passed to it.
Example
The implementation of an instance command called ActivateCard which manipulates the state of an instance of the root entity MasterCard is shown below:
@Service
public class MasterCardCommand extends MasterCardCommandBase {
@Override
public void activateCard(MasterCard instance) {
// Change root entity data
instance.setActive(true);
// Save to repository
repo.cc.masterCard.save(instance);
}
Business errors
- If a command is modelled with business errors, it will be added as method throws declaration.
Events
-
If a root entity command is modelled with business events, for each event there will be four inherited methods in the Command Base class to publish the event with the following signatures.
- Event only
- Event and messageKey
- Event and messageHeaders
- Event, messageKey and messageHeaders
-
It is also possible to publish events with any publish method mentioned above using EventProducerService directly.
Implementation Example
Example of root entity command implementation file.
Root Entity Card has a factory command createCreditCard and an instance command activateCreditCard.
//... imports
import myproj.sdk.domain.schemas.SchemaGroup.BalanceCheckedSchema;
@Service
public class CardCommand extends CardCommandBase {
private static Logger log = LoggerFactory.getLogger(CardCommand.class);
public CardCommand(DomainEntityBuilder entityBuilder, DomainEventBuilder eventBuilder, EventProducerService eventProducer, Repository repo) {
super(entityBuilder, eventBuilder, eventProducer, repo);
}
// Example of a factory command named createCreditCard that takes CreditCard as an input entity
@Override
public Card createCreditCard(CreditCard creditCard) throws CreditCardCreationError {
log.info("CardCommands.createCreditCard()");
// Use Domain Entity Builder from base class to create an instance of root entity
Card cardRootEntity = this.entityBuilder.getCc().getCard().build();
// Use card details data from the input entity
cardRootEntity.setCardDetails(creditCard);
cardRootEntity.setIsActive(false);
// If an Exception occurred while saving the new card, catch and throw relevant business error
try {
cardRootEntity = this.repo.getCc().getCard().save(cardRootEntity);
} catch(Exception e) {
log.error("Could not create a new credit card", e);
String errorMessage = String.format("Credit card creation failure, %s" , e.getMessag());
throw new CreditCardCreationError(errorMessage);
}
log.info("Saved a new Credit Card Root Entity with Id {}", cardRootEntity.getId());
// Publish event, if the event is using a schema from the schema registry as payload
BalanceCheckedEvent schemaEvent = this.eventBuilder.getCc().getBalanceCheckedEvent().build();
// Create the payload for the event
BalanceCheckedSchema schema = new BalanceCheckedSchema();
schema.setProperty1('value');
schemaEvent.setPayload(schema);
// Publish the event
this.publishBalanceCheckedEvent(schemaEvent);
/**
* Create an event and set payload entity, if the event is using an entity as payload
* only available for event support 1.0
* @deprecated use schemas from the schema registry instead
*/
CreditCardCreated event = this.eventBuilder.getCc().getCreditCardCreated().build();
event.setPayload(creditCard);
// Publish event using base class inherited method
this.publishCreditCardCreated(event);
log.info("Published New Credit Card Event Successfully");
return cardRootEntity;
}
// Example of an instance named activateCreditCard that takes only the Card root entity.
@Override
public void activateCreditCard(Card instance) {
log.info("CardCommands.activateCreditCard()");
// Activate Card
instance.setActive(true);
// Update root entity
this.repo.getCc().getCard().save(instance);
log.info("Activated Card with Id {}", instance.getId());
return;
}
// Example of calling inherited command from parent and not overriding its implementation.
@Override
public void deleteCard(Card instance) {
return super.deleteCard(instance);
}
}
Example for the alternative ways to publish event from command.
//... imports
import myproj.sdk.domain.schemas.SchemaGroup.BalanceCheckedSchema;
@Service
public class CardCommand extends CardCommandBase {
private static Logger log = LoggerFactory.getLogger(CardCommand.class);
public CardCommand(DomainEntityBuilder entityBuilder, DomainEventBuilder eventBuilder, EventProducerService eventProducer, Repository repo) {
super(entityBuilder, eventBuilder, eventProducer, repo);
}
@Override
public Card createCreditCard(CreditCard creditCard) throws CreditCardCreationError {
// create event
BalanceCheckedEvent schemaEvent = this.eventBuilder.getCc().getBalanceCheckedEvent().build();
// Create the payload for the event
BalanceCheckedSchema schema = new BalanceCheckedSchema();
schema.setProperty1("value");
schemaEvent.setPayload(schema);
// Publish the event
this.publishBalanceCheckedEvent(schemaEvent);
// publish the event with custom messageKey
this.publishBalanceCheckedEvent(schemaEvent, "customMessageKey");
// publish the event with messageHeaders
HashMap<String, Object> map = new HashMap();
map.put("headerKey", "headerValue");
MessageHeaders headers = new MessageHeaders(map);
this.publishBalanceCheckedEvent(schemaEvent, headers);
// publish the event with messageKey and messageHeaders
this.publishBalanceCheckedEvent(schemaEvent, "customMessageKey", headers);
/**
* Event 1.0 with entity payload
* @deprecated use schemas from the schema registry instead
*/
CreditCardCreated entityEvent = this.eventBuilder.getCc().getCreditCardCreated().build();
entityEvent.setPayload(creditCard);
// Publish event using base class inherited method
this.publishCreditCardCreated(event);
// Publish the event with messageHeaders
HashMap<String, Object> map = new HashMap();
map.put("headerKey", "headerValue");
MessageHeaders headers = new MessageHeaders(map);
this.publishBalanceUpdatedEvent(entityEvent, headers);
log.info("Published New Credit Card Event Successfully");
return cardRootEntity;
}
}
Message key will be ignored in case of publishing event 1.0 (Entity payload).
If you are using the aggregate persistence functionality, please ensure that your code is compliant with the recommended restrictions of the used database. Consult MongoDB documentation for further information. For example: Because of the limitations of database operations and drivers, MongoDB does not recommend the use of timestamps, that are not within the year range of 0 - 9999, see Date and DateTime documentation.
Trigger Commands
The generated SDK provides command facades which allow to easily trigger commands from other places in the service.
Namespace command facade
For each domain namespace, a facade is generated where all the commands defined in the namespace are grouped together. The facades can be injected and used as shown in the example below.
In the below example both (CcCommand & OpsCommand) are domain namespace facades that group all the commands in their respective namespaces where cc & ops are the namespace acronyms.
// Importing cc namespace command facade that provides access to all of its commands.
import de.cards.sdk.domain.cc.facade.CcCommand;
// Importing ops namespace service facade that provides access to all of its commands.
import de.cards.sdk.domain.ops.facade.OpsCommand;
// Declare and Inject Command Facades
@Autowired
private CcCommand ccCommand;
@Autowired
private OpsCommand opsCommand;
// Use namespace command facade to call UpdateCreditCardDetails & CreateProfile commands within that namespace
// Calling UpdateCreditCardDetails instance command that belongs to a root entity with local identifier "CreditCard"
CreditCard creditCardEntity = ccCommand.getCreditCard().updateCreditCardDetails(instance, cardDetails);
// Calling CreateProfile factory command that belongs to a root entity with local identifier "CustomerProfile"
CustomerProfile customerProfileEntity = opsCommand.getCustomerProfile().createProfile(customerData);
General command facade
Besides Namespace command facades, the SDK also provides a Command
facade which holds all commands in domain namespaces.
// Import overall Command facade class
import de.cards.sdk.domain.facade.Command;
// Declare & Inject Command Facade
@Autowired
private Command command;
// Calling commands inside a namespace with prefix "cc"
// UpdateCreditCardDetails is a command that belongs to a root entity with local identifier "CreditCard"
CreditCard creditCardEntity = command.getCc().getCreditCard().updateCreditCardDetails(instance, cardDetails);
Implement External Entities
Idea
-
An external entity modelled in Solution Designer acts as a pointer / reference to an entity that may reside in another system
-
Using a generated external entity service we can implement the logic for:
-
create, creates the external entity pointer / reference
-
load, loads the full details of that external system Entity and map it to one of modelled Domain Entities
-
validate, validates that the referenced Entity still exists in the external system and optionally update the External Entity data
-
External entity service base
-
For each external entity there will be an abstract class External Entity Service Base generated in the SDK
-
The External Entity Service Base provides access to the repository and entity builder
-
The External Entity Service Base contains three abstract methods create, load, and validate
-
These three methods needs to be implemented in the generated implementation file for the External Entity
Create
The create function is used to construct a local instance of the External Entity and initialize the associated properties with certain pre-specified values that are provided as input parameters.
-
create method will take modelled constructor properties as a parameters.
-
constructor properties acts as identifiers to be able to retrieve full data external entity that might reside in an external system.
-
create method will return modelled External Entity as a return type.
Load
The load function implements logic to call external system and loads the full external entity data.
-
load method will take modelled External Entity as a parameter.
-
Depending on the logic, load can connect to external system, and retrieve full data of Entity, that is referenced by External Entity, then map it to one of Domain Entity.
-
load method will return an instance of an Entity.
Validate
The validate method is used to validate the stored entity information by loading the data from the external service. It is recommended to also implement the logic of updating the external entity within the validate method.
-
validate method will take modelled External Entity as first parameter.
-
validate method update flag helps to determine whether the External Entity should be updated with the result from the external service or not
Implementation example
Example of Balance External Entity service implementation file.
//... imports
@Service
public class BalanceService extends BalanceServiceBase {
private static Logger log = LoggerFactory.getLogger(BalanceService.class);
public BalanceService(DomainEntityBuilder entityBuilder, Repository repo){
super(entityBuilder,repo);
}
@Override
public Balance create(String customerId) {
log.info("BalanceService.create()");
// Make external calls using the customerId , to get the data of External Entity
RestTemplate restTemplate = new RestTemplate();
JsonObject responseObject = restTemplate
.getForObject("https://some-url/balance/"+ customerId, JsonObject.class);
Balance balance = this.entityBuilder.getCc().getBalance().build();
balance.setCustomerId(customerId);
// Extract properties from responseObject and set it to balance entity
return balance;
}
/**
* Load external Entity and return as an Entity
*
* @param externalEntity instance of Balance
* @return class that extends base type Entity
*/
@Override
public Entity load(Balance externalEntity) {
log.info("BalanceService.load()");
return null;
}
/**
* validate the existence of the external entity in the external system.
*
* @param externalEntity instance of Balance.
* @param update flag to indicate whether should an updated
* Balance instance be created and
* returned or not.
* @return Optional Balance instance to indicate whether
* external entity still exists in external system
*/
@Override
public Optional<Balance> validate(Balance externalEntity, boolean update) {
log.info("BalanceService.validate()");
// Make external calls to validate if external entity still exists
RestTemplate restTemplate = new RestTemplate();
JsonObject responseObject = restTemplate
.getForObject("https://some-url/has-balance/"+ externalEntity.getCustomerId(), JsonObject.class);
// Need to handle cases:
// 1. external entity no longer exists
// 2. If update is set to true, should update the @param *externalEntity* data and return it.
return Optional.of(externalEntity);
}
}
Repository
When aggregate persistence support is enabled, the SDK provides access to the database through a repository. Using this, the connection to the underlying database is automatically established. As Aggregates are considered as a persistable unit, a repository class is generated for each root entity which allows to read, save and delete data from the database.
Depending on your chosen database type "MongoDB" or "RDBMS", the base class of that repository will vary.
MongoDB persistence
-
Each
Root Entity
instance will be saved into a Mongo DB Collection. -
The
Repository Class
through its inheritance chain extendsSimpleMongoRepository \<T, ID\>
. -
The
SimpleMongoRepository
provides access to several functionalities such as findBy, findAll, count, save, delete....etc. -
You can directly inject root entity repository or use it using repo which is a repository facade that is available in commands, domain services, external entities and agents base classes.
Example of repository usage for different database operations.
// Build a credit card root entity that exists in a domain namespace prefixed by cc.
CreditCard creditCard = this.entityBuilder.getCc().creditCard()
.setBankName("MyBank")
.setCardType(CardType.debit)
.setCustomer(customer)
.setIssueDate(OffsetDateTime.now())
.build();
// Save a credit card root entity.
creditCard = this.repo.getCc().getCreditCard().save(creditCard);
// Retrieve all credit card root entities
List<CreditCard> cards = this.repo.getCc().getCreditCard().findAll();
// Retrieve all credit card root entities filtered by CardType
ExampleMatcher matcher = ExampleMatcher.matchingAll().withIgnorePaths("BankName", "Customer", "IssueDate");
Example<CreditCard> example = Example.of(creditCard, matcher);
List<CreditCard> queryResult = repo.getCc().getCreditCard().findAll(example);
// Update credit card root entity
creditCard.setCardType(CardType.credit);
repo.getCc().getCreditCard().save(creditCard);
// Delete credit card root entity
repo.getCc().getCreditCard().delete(creditCard);
//Count all credit card root entities
long count = this.repo.getCc().getCreditCard().count();
Example of using inside a command implementation using declared repository from command base class.
//... imports
@Service
public class CardCommand extends CardCommandBase {
private static Logger log = LoggerFactory.getLogger(CardCommand.class);
public CardCommand(DomainEntityBuilder entityBuilder, DomainEventBuilder eventBuilder, EventProducerService eventProducer, Repository repo) {
super(entityBuilder, eventBuilder, eventProducer, repo);
}
@Override
public Card createCreditCard(CreditCard creditCard) throws CreditCardCreationError {
log.info("CardCommands.createCreditCard()");
// ..... command logic
// Using this.repo to find and save an updated Card root entity
try {
Optional<Card> cardRootEntity = this.repo.getCc().getCard().findById(creditCard.getId());
// Update card if it exists
if(cardRootEntity.isPresent()){
cardRootEntity.setActive(true);
log.info("Updated Credit Card Root Entity with Id {}", cardRootEntity.getId());
// Save updated card
this.repo.getCc().getCard().save(cardRootEntity);
} else {
throw CardNotFoundException("Could not find card with Id {}", creditCard.getid());
}
} catch(Exception e) {
// ... Exception handling logic
}
return cardRootEntity;
}
Example of injecting and using CardRepository in a mapper utility.
//... imports
// Importing *CardRepository*
import de.knowis.cards.sdk.domain.card.repository.CardRepository;
@Service
public class Mapper {
private static Logger log = LoggerFactory.getLogger(Mapper.class);
// Injecting *CardRepository*
@Autowired
private CardRepository cardRepo;
@Override
public Optional<Card> mapToRootEntity(String cardId){
Optional<Card> card= cardRepo.findById(cardId);
return card;
}
For more information about the functions of generated repositories that is inherited from SimpleMongoRepository See Official Spring Documentation
Repository find / count operations on a Root Entity that has child entities, will include these children as well.
MongoDB inheritance support
-
Within MongoDB Persitence support, multiple inheritance is supported through interfaces and concrete classes.
-
Root Entity
will have a corresponding interface that extends all its modelled parents, that interface will be implemented by a single concrete class.
public interface RelationRole extends ManagerialRelation, MarketingRelation {
String getRoleType();
void setRoleType(String roleType);
BigDecimal getShare();
void setShare(BigDecimal share);
}
- Finally the concrete class that represents that entity will implement that interface (i.e. will have properties of its own and those inherited from parent entities).
public class RelationRoleEntity extends EntityBase implements RelationRole {
public RelationRoleEntity(){}
@Field(name="roleType")
@NotEmpty
private String roleType;
@Field(name="share", targetType = FieldType.DECIMAL128)
private BigDecimal share;
RelationRoleEntity(RelationRoleBuilder builder) {
this.roleType = builder.roleType;
this.share = builder.share;
}
RDBMS persistence
-
The
Repository Interface
through its inheritance chain extends interfaceJpaRepository \<T, ID\>
-
The
JpaRepository
provide access to several functionalities such as findBy, findAll, count, save, delete....etc. -
You can directly inject root entity repository or use it using repo which is a repository facade that is available in commands, domain services, external entities and agents base classes.
Example of repository usage for different database operations.
// Build a credit card root entity that exists in a domain namespace prefixed by cc.
CreditCardEntity creditCard = this.entityBuilder.getCc().getCreditCard()
.setBankName("MyBank")
.setCardType(CardType.debit)
.setCustomer(customer)
.setIssueDate(OffsetDateTime.now())
.build();
// Save a credit card root entity.
creditCard = this.repo.getCc().getCreditCard().save(creditCard);
// Retrieve all credit card root entities
List<CreditCardEntity> cards = this.repo.cc.creditCard.findAll();
// Update credit card root entity
creditCard.setCardType(CardType.credit);
this.repo.getCc().getCreditCard().save(creditCard);
// Delete credit card root entity
this.repo.getCc().getCreditCard().delete(creditCard);
//Count all credit card root entities
long count = this.repo.getCc().getCreditCard().count();
Example of using inside a command implementation using declared repository from command base class.
//... imports
@Service
public class CardCommand extends CardCommandBase {
private static Logger log = LoggerFactory.getLogger(CardCommand.class);
public CardCommand(DomainEntityBuilder entityBuilder, DomainEventBuilder eventBuilder, EventProducerService eventProducer, Repository repo) {
super(entityBuilder, eventBuilder, eventProducer, repo);
}
@Override
public Card createCreditCard(CreditCard creditCard) throws CreditCardCreationError {
log.info("CardCommands.createCreditCard()");
// ..... command logic
// Using this.repo to find and save an updated Card root entity
try {
Optional<CardEntity> cardRootEntity =
this.repo.getCc().getCard().findById(creditCard.getId());
// Update card if it exists
if(cardRootEntity.isPresent()){
cardRootEntity.setActive(true);
log.info("Updated Credit Card Root Entity with Id {}", cardRootEntity.getId());
// Save updated card
this.repo.getCc().getCard().save(cardRootEntity);
} else {
throw CardNotFoundException("Could not find card with Id {}", creditCard.getid());
}
} catch(Exception e) {
// ... Exception handling logic
}
return cardRootEntity;
}
Example of injecting and using CardRepository in a mapper utility.
//... imports
// Importing *CardRepository*
import de.knowis.cards.sdk.domain.card.repository.CardRepository;
@Service
public class Mapper {
private static Logger log = LoggerFactory.getLogger(Mapper.class);
// Injecting *CardRepository*
@Autowired
private CardRepository cardRepo;
@Override
public Optional<CardEntity> mapToRootEntity(String cardId){
Optional<CardEntity> card= cardRepo.findById(cardId);
return card;
}
For more information about the functions of generated repositories that is inherited from JpaRepository See Official Spring Documentation
Repository find / count operations on a Root Entity that has child entities, will include these children as well.
RDBMS inheritance support
-
Inheritance is one of the key concepts in Java, and it's used in most domain models.
-
SQL doesn't support this kind of relationship, SDK used the JPA single table inheritance strategy to map inheritance between Entities.
-
Within RDBMS multiple inheritance is not supported,
Root Entity
can only have one parent. -
Each
Root Entity
will have an associated table name. -
Root Entities
that have parent(s), their upmost parent table name will be used as this is the JPA implementation for single table inheritance strategy.
Single table inheritance strategy
- Single Table inheritance: the simplest and typically the best performance when doing queries.
- Single Database Table: will be used to store every attribute of every class in the hierarchy.
- Discriminator Column: used to determine which class the row belongs to, each class in the hierarchy defines its own unique discriminator value.
Entity inheritance model example
Single table inheritance example
Mapping in RDBMS
Unlike MongoDB persistence support see MongoDB persitence support, where there is only db references used See Official mongo DBRef documentation
RDBMS have relationships between tables, below is how the relationships between entities is reflected in persistence.
Root Entity
Mapped to its own tables with support for One-To-Many and One-To-One Foreign Key relationships with other root entities.
(Value) Entity
Common use case as value objects, input to commands, services, etc.
If modelled as a property of a root entity, its data will be embedded in the root entity's table. In case of a collection (list of value entities), a collection table will be created and linked with root entity.
External Entity
Common use case as reference to an entity that resides outside the domain (or even within an external system). The same embedded strategy that applies to value entities will be applied here.
Mapping example
Below is a representation of an entity hierarchy and how its mapped into a DB2 RDBMS.
Spring Open EntityManager in View
The Spring Open Session In View (OSIV) pattern is used to keep a Hibernate session open during a web request, allowing lazy-loaded associations to be fetched when needed. This pattern is enabled by default and can lead to performance issues and memory leaks because it extends the session lifecycle beyond typical transaction boundaries, which is why it is often discouraged for high-concurrency applications. Consequently, Spring logs a warning message at startup to alert developers about the potential risks and to encourage explicit configuration of this feature. For more information, see official Spring documentation and potential issues related to this pattern.
MongoDB Configuration
Overwriting default naming strategy
To overwrite the default naming strategy use spring.data.mongodb.field-naming-strategy
. For example configure your application.yml
like this to set the naming strategy to snake case:
spring:
data:
mongodb:
field-naming-strategy: "org.springframework.data.mapping.model.SnakeCaseFieldNamingStrategy"
For further details and already existing default implementation feel free to have a look into the JavaDoc Spring-Data Interface FieldNamingStrategy .
RDBMS deployment prerequisites
For service projects with RDBMS support before running your pipeline(s) and deploying the service project you need to do some of the below tasks
- Provide the database connection
- Provide the JPA configuration
- Generate Database DDL (optional)
Make sure your database schema and tables are already reviewed and created by your DB admin!
To generate your DB ddl files see Generating ddl
Database connection
You need to provide a connection to the database that will be used by the service project. This can be done either by
adding connection properties in you service project's application.yaml
spring.datasource:
url: "jdbc:db2://<host>:<port>/<database-name>"
username: <user name>
password: <password>
driver-class-name: com.ibm.db2.jcc.DB2Driver
or by adding a configuration class that creates the necessary datasource bean programmatically:
@Configuration
public class DataSourceConfig {
@Bean
public DataSource getDataSource() {
DataSourceBuilder dataSourceBuilder = DataSourceBuilder.create();
dataSourceBuilder.driverClassName("com.ibm.db2.jcc.DB2Driver");
dataSourceBuilder.url("<database-url>");
dataSourceBuilder.username("<database-username>");
dataSourceBuilder.password("<database-password");
return dataSourceBuilder.build();
}
}
JPA configuration
Overwriting naming strategy (Java Spring Boot Stack 2.0)
If the naming strategy isn't configured in your k5-project.yml, it will be treated as legacy
. Hence the spring defaults are overwritten as following in the application.yml
:
spring.jpa:
hibernate:
naming:
implicit-strategy: org.hibernate.boot.model.naming.ImplicitNamingStrategyComponentPathImpl
physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
The naming of tables and columns is also set by default e.g. a column for the property 'age' will a @Column(name="age")
annotation, if nothing else is modelled in the designer.
To disable this legacy support and to switch to the spring defaults, set namingStrategies
to default
in your k5-project.yml
.
aggregatePersistenceSupport:
databaseType: DB2
enabled: true
namingStrategies: default
Now you can overwrite the defaults by changing the implicit-strategy
and the physical-strategy
field in your application.yml
. For further details please feel free to read the section Configure Hibernate Naming Strategy of 'Spring Boot Reference Documentation'.
Hibernate 6.6.4 Restriction on Primary Key Assignment: With Hibernate 6.6.4, assigning a custom value to a primary key that is annotated with @GeneratedValue(strategy = GenerationType.AUTO) or @GeneratedValue(strategy = GenerationType.IDENTITY) is now restricted.
Impact: Previously, it was possible to set a custom primary key value in the stubs despite using @GeneratedValue. In Hibernate 6.6.4, this is now explicitly restricted, and an exception is thrown if attempted.
Recommendation: Ensure that entities using @GeneratedValue do not have manually assigned primary key values. If explicit assignment is necessary, consider using a different strategy like GenerationType.SEQUENCE or manually managing ID assignment. This change is not a bug but rather an enforced validation in Hibernate that prevents incorrect implementations. Developers should review their entity configurations to avoid breaking changes.
Projects created after 4.1.1 automatically have the default naming strategy applied.
Naming collision in entities with default naming strategy
Naming collisions can occur if you are embedding two entities that have the same property name. You can solve this by adding arbitrary annotations JPA.
An example for this is, if the same entity is embedded several times within an entity (e.g. having a delivery address and a billing address).
To handle this, please add @jakarta.persistence.AttributeOverride
as custom annotation like the following example.
@jakarta.persistence.AttributeOverride(name="location", column=@Column("delivery_location"))
@jakarta.persistence.AttributeOverride(name="location", column=@Column("billing_location"))
Generating database DDL files
Java Spring Boot Stack 2.0
- You can generate database DDL scripts for your entities by using the
SchemaGenerator
class found in the base package, the same where your application class is located - To generate
.ddl
scripts, run the "main" method of the class from within your IDE - Default configuration parameters can be overwritten by your project's
application.yaml
orapplication-local.yaml
- You may also overwrite the class at your own volition
Java Spring Boot Stack 1.0 (Deprecated)
- You can generate database DDL scripts for your entities by configuring and using the Maven plugin jpa2ddl included in your service project's
pom.xml
file - You need to provide "SCHEMA_NAME" to be used in the
.ddl
scripts - You can generate
.ddl
scripts by running the "generate" task of the plugin from within your IDE - You need to provide the
.ddl
scripts to your database administrator and ask for them to be reviewed and your database schema and tables to be created
<plugin>
<groupId>com.devskiller.jpa2ddl</groupId>
<artifactId>jpa2ddl-maven-plugin</artifactId>
<version>0.9.12</version>
<extensions>false</extensions>
<!-- required to determine whether to run automatically or not -->
<configuration>
<outputPath>${basedir}/src/main/resources/database.sql</outputPath>
<packages>
<package>de.knowis.kb.demo.kbdemo.sdk.domain</package>
</packages>
<jpaProperties>
<property>
<name>hibernate.dialect</name>
<value>org.hibernate.dialect.DB2Dialect</value>
</property>
<property>
<name>hibernate.implicit_naming_strategy</name>
<value>org.hibernate.boot.model.naming.ImplicitNamingStrategyComponentPathImpl</value>
</property>
<property>
<name>hibernate.physical_naming_strategy</name>
<value>org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl</value>
</property>
<property>
<name>hibernate.default_schema</name>
<value>SCHEMA_NAME</value>
</property>
</jpaProperties>
<formatOutput>true</formatOutput>
<delimiter>;</delimiter>
<action>CREATE</action>
</configuration>
</plugin>