DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Please enter at least three characters to search
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Zones

Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks

Last call! Secure your stack and shape the future! Help dev teams across the globe navigate their software supply chain security challenges.

Modernize your data layer. Learn how to design cloud-native database architectures to meet the evolving demands of AI and GenAI workloads.

Releasing software shouldn't be stressful or risky. Learn how to leverage progressive delivery techniques to ensure safer deployments.

Avoid machine learning mistakes and boost model performance! Discover key ML patterns, anti-patterns, data strategies, and more.

Related

  • JSON-Based Serialized LOB Pattern
  • How to Restore a Transaction Log Backup in SQL Server
  • How to Attach SQL Database Without a Transaction Log File
  • A Deep Dive into Apache Doris Indexes

Trending

  • Teradata Performance and Skew Prevention Tips
  • Microsoft Azure Synapse Analytics: Scaling Hurdles and Limitations
  • Understanding Java Signals
  • Solid Testing Strategies for Salesforce Releases
  1. DZone
  2. Data Engineering
  3. Databases
  4. Runtime-Defined Columns With asentinel-orm

Runtime-Defined Columns With asentinel-orm

Working with runtime entity attributes mapped to standard database columns that are handled using standard SQL queries, generated directly via the ORM.

By 
Razvan Popian user avatar
Razvan Popian
·
Horatiu Dan user avatar
Horatiu Dan
·
Dec. 05, 24 · Tutorial
Likes (2)
Comment
Save
Tweet
Share
1.6K Views

Join the DZone community and get the full member experience.

Join For Free

Asentinel-orm is a lightweight ORM tool built on top of Spring JDBC, particularly JdbcTemplate.  Thus, it possesses most of the features one would expect from a basic ORM, such as SQL generation, lazy loading, etc.

By leveraging the JdbcTemplate, it means it allows participation in Spring-managed transactions, and it can be easily integrated into any project that already uses JdbcTemplate as a means to interact with the database.

Since 2015, asentinel-orm has been successfully used in several applications and continually improved as required by business needs. In the summer of 2024, it officially became an open-source project, which we consider will accelerate its evolution and increase the number of contributors.

In this article, a sample application is built to outline several ORM key features:

  • Simple configuration
  • Straightforward domain entity modeling via custom annotations
  • Easy writing and secure execution of plain SQL statements
  • Automatic SQL statements’ generation
  • Dynamic schema (entities are enriched with additional runtime attributes, persisted, and read without code changes)

Application

Setup

  • Java 21
  • Spring Boot 3.4.0
  • asentinel-orm 1.70.0
  • H2 database

Configuration

In order to interact with the asentinel-orm and leverage its functionalities, an instance of OrmOperations is required.

As stated in the JavaDoc, this is the central interface for performing ORM operations, and it is neither intended nor required to be specifically implemented in the client code.

The sample application includes the configuration code to create a bean of this type.

Java
 
@Bean
public OrmOperations orm(SqlBuilderFactory sqlBuilderFactory,
                             JdbcFlavor jdbcFlavor, SqlQuery sqlQuery) {
    return new OrmTemplate(sqlBuilderFactory, new SimpleUpdater(jdbcFlavor, sqlQuery));
}


OrmOperations has two super interfaces:

  • SqlBuilderFactory – creates SqlBuilder instances that can be further used to create SQL queries. SqlBuilder is able to auto-generate parts of the query, for instance, the one that selects the columns. The where clause, the order by clause, other conditions, and the actual columns can be added using methods from the SqlBuilder class as well. In the next part of this section, a SqlBuilder generated query example is shown.
  • Updater – used for saving entities to their respective database tables. It can perform inserts or updates depending on whether the entity is newly created or already existing. A strategy interface called NewEntityDetector exists, which is used for determining whether an entity is a new one. By default, the SimpleNewEntityDetector is used.

All queries generated by the ORM are executed using an SqlQueryTemplate instance, which further needs a Spring JdbcOperations/JdbcTemplate to work. Eventually, all queries reach the good old JdbcTemplate through which they are executed while participating in Spring transactions, just as any JdbcTemplate direct execution.

Database specific SQL constructs and logic are provided via implementations of the JdbcFlavor interface, further injected into most of the beans mentioned above. In this article, as an H2 database is used, a H2JdbcFlavor implementation is configured.

The complete configuration of the ORM as part of the sample application is OrmConfig.

Implementation

The experimental domain model exposed by the sample application is straightforward and consists of two entities – car manufacturers and car models. Representing exactly what their names denote, the relationship between them is obvious: one car manufacturer may own multiple car models. 

In addition to its name, the car manufacturer is enriched with attributes (columns) that are input by the application user dynamically at runtime. The exemplified use-case is straight-forward:

  • The user is requested to provide the aimed names and types for the dynamic attributes
  • A couple of car manufacturers are created with concrete values for previously added dynamic attributes, and then
  • The entities are loaded back described by both the initial and the runtime-defined attributes

The initial entities are mapped using the database tables below:

SQL
 
CREATE TABLE CarManufacturers (
    ID INT auto_increment PRIMARY KEY,
    NAME VARCHAR(255)
);
 
CREATE TABLE CarModels(
    ID INT auto_increment PRIMARY KEY,
    CarManufacturer int,
    NAME VARCHAR(255),
    TYPE VARCHAR(15),
    foreign key (CarManufacturer) references CarManufacturers(id)
);


The corresponding domain classes are decorated with ORM-specific annotations to configure the mappings to the above database tables. 

Java
 
@Table("CarManufacturers")
public class CarManufacturer {
     
    @PkColumn("id")
    private int id;
     
    @Column("name")
    private String name;
     
    @Child(parentRelationType = RelationType.MANY_TO_ONE, 
            fkName = CarModel.COL_CAR_MANUFACTURER, 
            fetchType = FetchType.LAZY)
    private List<CarModel> models = Collections.emptyList();
 
    ... 
}
Java
 
@Table("CarModels")
public class CarModel {
     
    public static final String COL_CAR_MANUFACTURER = "CarManufacturer";
     
    @PkColumn("id")
    private int id;
     
    @Column("name")
    private String name;
     
    @Column("type")
    private CarType type;
     
    @Child(fkName = COL_CAR_MANUFACTURER, fetchType = FetchType.LAZY)
    private CarManufacturer carManufacturer;
 
    ...
}
 
public enum CarType {
    CAR, SUV, TRUCK
}


A few considerations:

  • @Table – maps (associates) the class to a database table
  • @PkColumn – maps the id (unique identifier) to the table primary key
  • @Column – maps a class member to a table column 
  • @Child – defines the relationship with another entity
  • @Child annotated members – configured to be lazily loaded
  • type table column – mapped to an enum field – CarType

In order for the CarManufacturer class to support runtime-defined attributes (mapped to runtime-defined table columns), a subclass as the one below is defined:

Java
 
public class CustomFieldsCarManufacturer extends CarManufacturer 
            implements DynamicColumnsEntity<DynamicColumn> {
     
    private final Map<DynamicColumn, Object> customFields = new HashMap<>();
   	...
 
    @Override
    public void setValue(DynamicColumn column, Object value) {
        customFields.put(column, value);
         
    }
 
    @Override
    public Object getValue(DynamicColumn column) {
        return customFields.get(column);
    }
    ...
}


This class stores the runtime-defined attributes (fields) in a Map. The interaction between the runtime field values and the ORM is fulfilled via the implementation of the DynamicColumnEntity interface. 

Java
 
public interface DynamicColumnsEntity<T extends DynamicColumn> {
  
    void setValue(T column, Object value);
  
    Object getValue(T column);
}


  • setValue() – is used to set the value of the runtime-defined column when this is read from the table
  • getValue() – is used to retrieve the value of a runtime-defined column when this is saved to the table

The DynamicColumn maps runtime-defined attributes to their corresponding columns in a similar manner to the @Column annotation maps compile time known members.

When running the application, the CfRunner is executed. The user is asked to input names and types for the desired dynamic custom attributes that enrich the CarManufacturer entity (for simplicity, only int and varchar types are supported). 

For each name–type pair, a DML command is executed so that the new columns can be added to the CarManufacturer database table. The following method (declared in CarService) performs the operation.

Java
 
@Transactional
public void addManufacturerField(String name, String type) {
	orm.getSqlQuery()
           .update("alter table CarManufacturers add column " +  name + " " + type);
}


Each input attribute is recorded as a DefaultDynamicColumn, a DynamicColumn reference implementation.

Once all attributes are defined, two car manufacturers are added to the database, as the user provides values for each such attribute.

Java
 
Map<DynamicColumn, Object> dynamicColumnsValues = new HashMap<>();
for (DynamicColumn dynamicColumn : dynamicColumns) {
    // read values for each dynamic attribute
    ...
}
         
CustomFieldsCarManufacturer mazda = new CustomFieldsCarManufacturer("Mazda", dynamicColumnsValues);
 
carService.createManufacturer(mazda, dynamicColumns);


The below method (declared in CarService) actually creates the entity via the ORM. 

Java
 
@Transactional
public void createManufacturer(CustomFieldsCarManufacturer manufacturer, List<DynamicColumn> attributes) {
    orm.update(manufacturer, new UpdateSettings<>(attributes, null));
}


The 2 parameter version of the OrmOperations update() method is called, which allows passing an UpdateSettings instance and communicating to the ORM upon execution that there are runtime-defined whose values shall be persisted. 

Lastly, two car models are created, corresponding to one of the previously added car manufacturers.

Java
 
CarModel mx5 = new CarModel("MX5", CarType.CAR, mazda);
CarModel cx60 = new CarModel("CX60", CarType.SUV, mazda);
carService.createModels(mx5, cx60);


The below method (declared in CarService) actually creates the entities via the ORM, this time using OrmOperations update() method for persisting entities without dynamic attributes. For convenience, multiple entities are created in one call. 

Java
 
@Transactional
public void createModels(CarModel... models) {
    orm.update(models);
}


As a last step, one of the created manufacturers is loaded back by its name using an ORM-generated query. 

Java
 
CarManufacturer mazda1 = carService.findManufacturerByName("Mazda", dynamicColumns);
 
@Transactional(readOnly = true)
public CarManufacturer findManufacturerByName(String name, List<DynamicColumn> attributes) {
    return orm.newSqlBuilder(CustomFieldsCarManufacturer.class)
            .select(
                    AutoEagerLoader.forPath(CarManufacturer.class, CarModel.class),
                    new DynamicColumnsEntityNodeCallback<>(
                            new DefaultObjectFactory<>(CustomFieldsCarManufacturer.class),
                            attributes
                    )
            )
            .where().column("name").eq(name)
        .execForEntity();
}


A few explanations regarding the method defined above are worth doing.

The OrmOperations newSqlBuilder() method creates a SqlBuilder instance, and as the name suggests, this can be used to generate SQL queries. The SqlBuilder select() method generates the select from table part of the query, while the rest (where, order by) must be added. The query select part can be customized by passing EntityDescriptorNodeCallback instances (details on EntityDescriptorNodeCallback may be the subject of a future article). 

In order to let the ORM know that the plan is to select and map runtime-defined columns, a DynamicColumnsEntityNodeCallback needs to be passed. Together with it, an AutoEagerLoader one is provided so that the ORM understands to eagerly load the list of CarModels associated with the manufacturer. Nevertheless, this has nothing to do with the runtime-defined attributes, but it demonstrates how a child member can be eagerly loaded.

Conclusion

While there are probably other ways of working with runtime-defined columns when data is stored in relational databases, the approach presented in this article has the advantage of using standard database columns that are read/written using standard SQL queries generated directly by the ORM.

It wasn’t rare when we had the chance to discuss in “the community” the asentinel-orm, the reasons we had to develop such a tool. Usually, at first glance, developers proved to be reluctant and reserved when it came to a custom-made ORM, asking why not using Hibernate or other JPA implementations. 

In our case, the main driver was the need for a fast, flexible, and easy way of working with sometimes quite a big number of runtime-defined attributes (columns) for entities that are part of the business domain. For us, it proved to be the right way. The applications are running smoothly in production, the customers are happy with the speed and the achieved performance, and the developers are comfortable and creative with the intuitive API.

As the project is now open-source, it is very easy for anyone interested to have a look, form an objective opinion about it, and, why not, fork it, open a PR, and contribute.

Resources

  • The open-source ORM project is here.
  • The source code of the sample application is here.
Database Attribute (computing) sql

Published at DZone with permission of Razvan Popian. See the original article here.

Opinions expressed by DZone contributors are their own.

Related

  • JSON-Based Serialized LOB Pattern
  • How to Restore a Transaction Log Backup in SQL Server
  • How to Attach SQL Database Without a Transaction Log File
  • A Deep Dive into Apache Doris Indexes

Partner Resources

×

Comments
Oops! Something Went Wrong

The likes didn't load as expected. Please refresh the page and try again.

ABOUT US

  • About DZone
  • Support and feedback
  • Community research
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 100
  • Nashville, TN 37211
  • support@dzone.com

Let's be friends:

Likes
There are no likes...yet! 👀
Be the first to like this post!
It looks like you're not logged in.
Sign in to see who liked this post!