📚 Official Documentation
English README | 中文 README
mybatis-dynamic is a powerful dynamic ORM framework built on top of MyBatis. It allows developers to define data models directly in Java code, automatically managing the database schema (tables, columns, indexes) and providing a rich, fluent API for CRUD operations and complex queries.
- Dynamic Modeling: Define data models as Java classes. The framework automatically generates and updates the database schema (DDL) at startup or runtime.
- Runtime Model Modification: Models can be modified programmatically at runtime, enabling dynamic business requirements.
- Rich Fluent API: Construct complex queries with a natural, readable syntax. Supports automatic logical precedence (
AND>OR), nested grouping, and joins. - Spring Boot Integration: Seamless integration with Spring Boot. Simply annotate your model, and the framework auto-configures
BaseDaoandBaseServicebeans. - Advanced Mapping: Supports
@ToOne,@ToMany, and recursive relationships. - Visualisation: Built-in tool to visualize model relationships.
The project is modularized to separate concerns:
core: The engine. Handles dynamic modeling, SQL generation, and query execution. Can be used standalone.spring: Spring Boot starter. Provides auto-configuration and easy integration.draw: Visualization module. Provides a web UI to view entity relationships.sample: A complete Spring Boot sample application demonstrating usage.
- Java 8+
- Maven or Gradle
- A supported database (H2, MySQL, PostgreSQL, Oracle, OceanBase, etc.)
Add the mybatis-dynamic-spring dependency to your project.
Maven:
<dependency>
<groupId>io.github.myacelw</groupId>
<artifactId>mybatis-dynamic-spring</artifactId>
<version>Latest Version</version>
</dependency>
<!-- Add your database driver, e.g., H2 -->
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>Configure your database and mybatis-dynamic in application.yml:
spring:
datasource:
driver-class-name: org.h2.Driver
url: jdbc:h2:./test;MODE=MySQL
username: sa
password:
mybatis-dynamic:
# Automatically update database schema based on models
update-model: true
# Table name prefix
table-prefix: t_Create a simple entity class annotated with @Model.
package com.example.demo.model;
import io.github.myacelw.mybatis.dynamic.core.annotation.Model;
import io.github.myacelw.mybatis.dynamic.core.annotation.BasicField;
import io.github.myacelw.mybatis.dynamic.core.annotation.IdField;
import lombok.Data;
import lombok.experimental.FieldNameConstants;
@Data
@FieldNameConstants
@Model(comment = "User Table")
public class User {
@IdField
private Integer id;
@BasicField(ddlComment = "User Name", ddlNotNull = true)
private String name;
@BasicField(ddlComment = "User Age")
private Integer age;
}Add @EnableModelScan to your Spring Boot application class.
@SpringBootApplication
@EnableModelScan(basePackages = "com.example.demo.model")
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}Inject the auto-generated BaseService or BaseDao to perform operations.
@RestController
@RequestMapping("/users")
public class UserController {
// Auto-injected service for the User model
@Autowired
private BaseService<Integer, User> userService;
@PostMapping
public Integer create(@RequestBody User user) {
return userService.insert(user);
}
@GetMapping
public List<User> list() {
// Fluent query API
return userService.query()
.where(c -> c.gt(User.Fields.age, 18))
.exec();
}
}The framework provides a generic DynamicModelController that automatically exposes REST endpoints for all registered models.
Endpoints:
GET /api/dynamic/{modelName}: List with pagination/filtering (?name=John&page=1&size=10).GET /api/dynamic/{modelName}/{id}: Get by ID.POST /api/dynamic/{modelName}: Create.PUT /api/dynamic/{modelName}: Update.DELETE /api/dynamic/{modelName}/{id}: Delete.
Configuration: Enabled by default. To disable:
mybatis-dynamic:
rest:
enabled: false-
@Model: Marks a class as a managed model.tableName: Custom table name (default: derived from class name).comment: Database table comment.logicDelete: Enable logical deletion.disableTableCreateAndAlter: Disable auto-DDL.
-
@IdField: Marks the primary key field.keyGeneratorMode: ID generation strategy.order: Required for Composite Primary Keys.ddlColumnType: Manually specify column type (e.g., "VARCHAR(64)").
-
@BasicField: Maps a field to a standard database column.columnName: Custom column name.ddlNotNull: Column cannot be null.ddlDefaultValue: Default value definition.ddlCharacterMaximumLength: Max length for string columns.ddlNumericPrecision/ddlNumericScale: Precision for numeric types.ddlIndex: Create an index.ddlIndexType: Type of index (NORMAL,UNIQUE).ddlComment: Column comment.
-
@ToOne: Defines a "Many-to-One" or "One-to-One" relationship.targetModel: Target model name (optional if field type is a Model).joinField: Foreign key field in this model (default:{fieldName}Id).
-
@ToMany: Defines a "One-to-Many" or "Many-to-Many" relationship.targetModel: Target model name (optional if List generic type is a Model).joinField: Foreign key field in the target model (default:{thisModelName}Id).
-
@IgnoreField: Excludes the field from database mapping.
A. One-to-One / Many-to-One
Example: A User belongs to a Department.
@Data
@Model
public class User {
@IdField
private String id;
private String name;
// Defines relation to Department.
// Expects "departmentId" column in User table.
@ToOne
private Department department;
}Querying (Auto-Join): Selecting a field from a related entity automatically triggers a Left Join.
// Automatically joins 'department' table to fetch department name
List<User> users = userService.query()
.select(User.Fields.name, "department.name")
.exec();Explicit Join Configuration:
You can customize the join type (e.g., INNER JOIN) and add ON conditions using the on() method.
// Customize join type and add conditions
List<User> users = userService.query()
.joins(Join.inner("mainDepartment")
.on(c -> c.eq("active", true)))
.exec();B. One-to-Many
Example: A Department has many Users.
@Data
@Model
public class Department {
@IdField
private String id;
private String name;
// Defines relation to Users.
// Expects "departmentId" column in User table.
@ToMany
private List<User> users;
}Querying:
// Fetch Department and all its Users
List<Department> depts = departmentService.query()
.joins(Join.of("users"))
.exec();C. Many-to-Many
Implemented using an Intermediate Entity with a composite primary key.
Example: Students and Courses.
// 1. Student
@Model
public class Student {
@IdField private String id;
// Relation to the intermediate table
@ToMany
private List<StudentCourse> studentCourses;
}
// 2. Course
@Model
public class Course {
@IdField private String id;
}
// 3. StudentCourse (Intermediate)
@Model
public class StudentCourse {
@IdField(order = 0) private String studentId;
@IdField(order = 1) private String courseId;
@ToOne private Course course;
}Querying:
// Fetch Student and their Courses (joining through StudentCourse)
List<Student> students = studentService.query()
.joins(Join.of("studentCourses.course"))
.exec();Annotate multiple fields with @IdField and specify their order.
@Model
public class UserRole {
@IdField(order = 0)
private String userId;
@IdField(order = 1)
private String roleId;
}Map an inheritance hierarchy to a single database table.
@Model(tableName = "person")
@SubTypes(subTypes = {@SubTypes.SubType(User.class), @SubTypes.SubType(Guest.class)}, subTypeFieldName = "type")
public abstract class Person {
@IdField
private String id;
private String name;
}
@Data
public class User extends Person {
private Integer age;
}
@Data
public class Guest extends Person {
private String token;
}Handle dynamic fields that are not explicitly defined in the Java class.
@Data
@Model
public class User implements ExtBean {
@IdField
private String id;
private String name;
@IgnoreField
private Map<String, Object> ext = new HashMap<>();
@Override
public Map<String, Object> getExt() {
return ext;
}
}Usage:
// 1. Add dynamic field definition at runtime
Model userModel = modelService.getModelForClass(User.class);
userModel.getFields().add(Field.string("phone", 100));
modelService.update(userModel);
// 2. Use it (Insert/Update/Query)
User user = new User();
user.getExt().put("phone", "123456");
userService.insert(user);
List<User> users = userService.query()
.where(c -> c.eq("phone", "123456"))
.exec();Construct complex queries with automatic logical precedence (AND > OR).
The ConditionBuilder supports a wide range of operators:
- Simple:
eq(equal),ne(not equal),gt(greater than),gte,lt,lte. - String:
like,startsWith,endsWith,contains. - Null Check:
isNull,isNotNull,isBlank,isNotBlank. - Collection:
in,notIn. - Logical:
and,or,not(nested). - Exists:
exists(Subquery).
Optional Variants:
All the above operators (except Null Check and Exists) have an Optional variant (e.g., eqOptional, likeOptional, inOptional).
These methods automatically ignore the condition if the provided value is null, an empty string, or an empty collection.
// If name is null, "name = ?" is not added to SQL.
// If age is null, "age > ?" is not added to SQL.
userService.query()
.where(c -> c.eqOptional("name", name)
.gtOptional("age", age))
.exec();Complex Logic
// WHERE (age > 18 AND status = 'Active') OR role = 'Admin'
userService.query()
.where(c -> c.bracket(b -> b.gt("age", 18).eq("status", "Active"))
.or(b -> b.eq("role", "Admin")))
.joins(Join.of("mainDepartment"))
.exec();Exists Subquery
// Find users who have at least one order with amount > 100
userService.query()
.where(c -> c.exists("orders", sub -> sub.gt("amount", 100)))
.exec();The DataManager interface supports Map<String, Object> for scenarios without entity classes.
// Get DataManager
DataManager<Integer> dataManager = modelService.getDataManager("User");
// Insert Map
Map<String, Object> data = new HashMap<>();
data.put("name", "Bob");
Integer id = dataManager.insert(data);
// Query returning Maps
List<Map<String, Object>> results = dataManager.query()
.where(c -> c.gt("age", 20))
.exec();Efficiently handle large datasets.
List<User> userList = ...;
// Batch Insert
userService.getDataManager().batchInsert(userList);
// Batch Update (by ID)
userService.getDataManager().batchUpdate(userList);
// Batch Update by Condition
userService.batchUpdateByConditionChain()
.add(c -> c.eq("status", "Active"), user1)
.add(c -> c.eq("status", "Inactive"), user2)
.exec();Perform SQL aggregate functions (COUNT, SUM, AVG, MAX, MIN) with grouping.
Use dataManager.aggQuery() to start an aggregation query chain.
// Count all users
Map<String, Object> result = userService.getDataManager()
.aggQuery()
.count() // Default: COUNT(*), alias "count"
.exec()
.get(0);
Long count = (Long) result.get("count");You can specify the field, alias, and grouping.
// Calculate average age and max age per department
// SQL: SELECT department_id as deptId, AVG(age) as avgAge, MAX(age) as maxAge FROM user GROUP BY department_id
List<Map<String, Object>> results = userService.getDataManager()
.aggQuery()
.groupBy("departmentId", "deptId")
.avg("age", "avgAge")
.max("age", "maxAge")
.exec();Use .where() to filter data before aggregation.
// Count users older than 18
List<Map<String, Object>> results = userService.getDataManager()
.aggQuery()
.count()
.where(c -> c.gt("age", 18))
.exec();Map results to a DTO class.
@Data
public class DeptStats {
private String deptId;
private Double avgAge;
}
List<DeptStats> stats = userService.getDataManager()
.aggQuery(DeptStats.class)
.groupBy("departmentId", "deptId")
.avg("age", "avgAge")
.exec();Retrieve hierarchical data (e.g., Department Tree).
Map<String, Object> tree = departmentService.getRecursiveTreeById("dept-id");Hook into data operations (beforeInsert, afterUpdate, etc.).
@Component
public class MyInterceptor implements DataChangeInterceptor {
@Override
public void beforeInsert(DataManager<Object> dataManager, Object data) {
// ...
}
}Automatically populate fields (e.g., createTime, updateUser). Implement Filler or extend AbstractCreatorFiller.
The framework provides a robust permission system to control access to data (Row Permissions) and fields (Column Permissions).
- Row Permissions (Data Rights): Filters data based on conditions (e.g.,
tenant_id = 'T001'). Applied automatically to Select, Update, and Delete operations. - Column Permissions (Field Rights): Restricts which fields are visible (in Select) or modifiable (in Insert/Update).
Implement the CurrentUserHolder interface to provide user context and permissions.
@Component
public class MyUserHolder implements CurrentUserHolder {
@Override
public String getCurrentUserId() {
// Return current user ID from Security Context (e.g., Spring Security)
return SecurityContextHolder.getContext().getAuthentication().getName();
}
@Override
public Permission getCurrentUserPermission(Model model) {
// Return permissions based on the model and current user
if ("User".equals(model.getName())) {
// Row Permission: Only see users in the same tenant
Condition dataRights = SimpleCondition.eq("tenant_id", getCurrentTenantId());
// Column Permission: Cannot see "password" or "salary"
// If list is null, all fields are accessible.
List<String> fieldRights = Arrays.asList("id", "name", "age", "tenant_id");
return new Permission(fieldRights, dataRights);
}
return null; // No restrictions
}
}Multi-tenancy is a primary use case for Row Permissions. By returning a dataRights condition (e.g., tenant_id = current_tenant_id) in getCurrentUserPermission, you ensure strict data isolation across the application.
1. Runtime Data Mapping (MyBatis TypeHandler)
Use standard MyBatis TypeHandler to convert data between Java and Database at runtime.
@BasicField(typeHandler = MyJsonTypeHandler.class)
private MyObject data;2. DDL Type Mapping (ColumnTypeHandler)
Control how a Java type maps to a Database Column Definition (e.g., VARCHAR(255)) during auto-DDL.
@Component
public class MyCustomTypeHandler implements ColumnTypeHandler {
@Override
public boolean doSetColumnType(Column column, Class<?> javaType, DataBaseDialect dialect, Field field) {
if (javaType == MyCustomType.class) {
column.setDataType("VARCHAR");
column.setCharacterMaximumLength(500);
return true; // Handled
}
return false;
}
}- Issues: Please file an issue for bugs or feature requests.
- Discussions: Join the discussions on GitHub.
This project is licensed under the Apache License 2.0.