https://github.com/myacelw/mybatis-dynamic
Code-first ORM for Spring Boot. Zero XML, Auto DDL & Fluent Join API. Define your schema in Java and enjoy dynamic SQL power.
https://github.com/myacelw/mybatis-dynamic
code-generator database dynamic-sql java mybatis orm spring-boot
Last synced: about 2 months ago
JSON representation
Code-first ORM for Spring Boot. Zero XML, Auto DDL & Fluent Join API. Define your schema in Java and enjoy dynamic SQL power.
- Host: GitHub
- URL: https://github.com/myacelw/mybatis-dynamic
- Owner: myacelw
- Created: 2026-01-13T09:28:20.000Z (5 months ago)
- Default Branch: master
- Last Pushed: 2026-02-10T03:25:16.000Z (4 months ago)
- Last Synced: 2026-02-10T07:35:24.705Z (4 months ago)
- Topics: code-generator, database, dynamic-sql, java, mybatis, orm, spring-boot
- Language: Java
- Size: 1.87 MB
- Stars: 1
- Watchers: 0
- Forks: 0
- Open Issues: 2
-
Metadata Files:
- Readme: README.md
- Contributing: CONTRIBUTING.md
- Agents: AGENTS.md
Awesome Lists containing this project
- fucking-awesome-java - mybatis-dynamic - Code-first dynamic ORM for MyBatis with runtime schema modification. (Projects / ORM)
- awesome-java - mybatis-dynamic - Code-first dynamic ORM for MyBatis with runtime schema modification. (Projects / ORM)
README
# mybatis-dynamic
π Official Documentation
English README | δΈζ README
[](LICENSE)
[](https://search.maven.org/search?q=g:io.github.myacelw%20AND%20a:mybatis-dynamic-core)
**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.
## Getting Started
### Key Features
- **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 `BaseDao` and `BaseService` beans.
- **Advanced Mapping**: Supports `@ToOne`, `@ToMany`, and recursive relationships.
- **Visualisation**: Built-in tool to visualize model relationships.
### Architecture
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.
### Installation & Configuration
#### 1. Requirements
- Java 8+
- Maven or Gradle
- A supported database (H2, MySQL, PostgreSQL, Oracle, OceanBase, etc.)
#### 2. Add Dependency
Add the `mybatis-dynamic-spring` dependency to your project.
**Maven:**
```xml
io.github.myacelw
mybatis-dynamic-spring
Latest Version
com.h2database
h2
runtime
```
#### 3. Configuration
Configure your database and `mybatis-dynamic` in `application.yml`:
```yaml
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_
```
### Your First Model
Create a simple entity class annotated with `@Model`.
```java
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;
}
```
### Enable Scanning
Add `@EnableModelScan` to your Spring Boot application class.
```java
@SpringBootApplication
@EnableModelScan(basePackages = "com.example.demo.model")
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
```
### Basic CRUD Usage
Inject the auto-generated `BaseService` or `BaseDao` to perform operations.
```java
@RestController
@RequestMapping("/users")
public class UserController {
// Auto-injected service for the User model
@Autowired
private BaseService userService;
@PostMapping
public Integer create(@RequestBody User user) {
return userService.insert(user);
}
@GetMapping
public List list() {
// Fluent query API
return userService.query()
.where(c -> c.gt(User.Fields.age, 18))
.exec();
}
}
```
### Zero-Code REST API
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:
```yaml
mybatis-dynamic:
rest:
enabled: false
```
## Advanced Data Management
### Advanced Entity Modeling
#### Annotations Reference
- **`@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.
#### Relationships & Querying
**A. One-to-One / Many-to-One**
*Example: A User belongs to a Department.*
```java
@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**.
```java
// Automatically joins 'department' table to fetch department name
List 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.
```java
// Customize join type and add conditions
List users = userService.query()
.joins(Join.inner("mainDepartment")
.on(c -> c.eq("active", true)))
.exec();
```
**B. One-to-Many**
*Example: A Department has many Users.*
```java
@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 users;
}
```
**Querying:**
```java
// Fetch Department and all its Users
List 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.*
```java
// 1. Student
@Model
public class Student {
@IdField private String id;
// Relation to the intermediate table
@ToMany
private List 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:**
```java
// Fetch Student and their Courses (joining through StudentCourse)
List students = studentService.query()
.joins(Join.of("studentCourses.course"))
.exec();
```
#### Composite Primary Keys
Annotate multiple fields with `@IdField` and specify their `order`.
```java
@Model
public class UserRole {
@IdField(order = 0)
private String userId;
@IdField(order = 1)
private String roleId;
}
```
#### Inheritance (Single Table)
Map an inheritance hierarchy to a single database table.
```java
@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;
}
```
#### Extensibility (`ExtBean`)
Handle dynamic fields that are not explicitly defined in the Java class.
```java
@Data
@Model
public class User implements ExtBean {
@IdField
private String id;
private String name;
@IgnoreField
private Map ext = new HashMap<>();
@Override
public Map getExt() {
return ext;
}
}
```
**Usage:**
```java
// 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 users = userService.query()
.where(c -> c.eq("phone", "123456"))
.exec();
```
### Fluent Query API
Construct complex queries with automatic logical precedence (`AND` > `OR`).
#### 1. Condition Operators
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.
```java
// 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();
```
#### 2. Examples
**Complex Logic**
```java
// 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**
```java
// Find users who have at least one order with amount > 100
userService.query()
.where(c -> c.exists("orders", sub -> sub.gt("amount", 100)))
.exec();
```
### DataManager with Maps
The `DataManager` interface supports `Map` for scenarios without entity classes.
```java
// Get DataManager
DataManager dataManager = modelService.getDataManager("User");
// Insert Map
Map data = new HashMap<>();
data.put("name", "Bob");
Integer id = dataManager.insert(data);
// Query returning Maps
List> results = dataManager.query()
.where(c -> c.gt("age", 20))
.exec();
```
### Batch Operations
Efficiently handle large datasets.
```java
List 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();
```
### Aggregation Queries
Perform SQL aggregate functions (COUNT, SUM, AVG, MAX, MIN) with grouping.
#### 1. Basic Usage
Use `dataManager.aggQuery()` to start an aggregation query chain.
```java
// Count all users
Map result = userService.getDataManager()
.aggQuery()
.count() // Default: COUNT(*), alias "count"
.exec()
.get(0);
Long count = (Long) result.get("count");
```
#### 2. Aggregation Functions & Grouping
You can specify the field, alias, and grouping.
```java
// 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> results = userService.getDataManager()
.aggQuery()
.groupBy("departmentId", "deptId")
.avg("age", "avgAge")
.max("age", "maxAge")
.exec();
```
#### 3. Filtering
Use `.where()` to filter data *before* aggregation.
```java
// Count users older than 18
List> results = userService.getDataManager()
.aggQuery()
.count()
.where(c -> c.gt("age", 18))
.exec();
```
#### 4. Result Mapping
Map results to a DTO class.
```java
@Data
public class DeptStats {
private String deptId;
private Double avgAge;
}
List stats = userService.getDataManager()
.aggQuery(DeptStats.class)
.groupBy("departmentId", "deptId")
.avg("age", "avgAge")
.exec();
```
### Recursive Queries
Retrieve hierarchical data (e.g., Department Tree).
```java
Map tree = departmentService.getRecursiveTreeById("dept-id");
```
## Expert Features
### Interceptors
Hook into data operations (`beforeInsert`, `afterUpdate`, etc.).
```java
@Component
public class MyInterceptor implements DataChangeInterceptor {
@Override
public void beforeInsert(DataManager dataManager, Object data) {
// ...
}
}
```
### Auto-Fillers
Automatically populate fields (e.g., `createTime`, `updateUser`). Implement `Filler` or extend `AbstractCreatorFiller`.
### Permission Management
The framework provides a robust permission system to control access to data (Row Permissions) and fields (Column Permissions).
#### 1. Concepts
- **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).
#### 2. Implementing Permissions
Implement the `CurrentUserHolder` interface to provide user context and permissions.
```java
@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 fieldRights = Arrays.asList("id", "name", "age", "tenant_id");
return new Permission(fieldRights, dataRights);
}
return null; // No restrictions
}
}
```
#### 3. Multi-Tenancy
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.
### Custom Type Handlers
**1. Runtime Data Mapping (MyBatis TypeHandler)**
Use standard MyBatis `TypeHandler` to convert data between Java and Database at runtime.
```java
@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.
```java
@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;
}
}
```
## Community
- **Issues**: Please file an issue for bugs or feature requests.
- **Discussions**: Join the discussions on GitHub.
## License
This project is licensed under the [Apache License 2.0](LICENSE).