MyBatis-Flex vs MyBatis-Plus: A Detailed Source‑Code and Feature Comparison
This article walks through a side‑by‑side examination of MyBatis‑Flex and MyBatis‑Plus, covering entity annotations, query construction, pagination, architectural differences, APT‑generated code, multi‑table joins, partial updates, and the trade‑offs of each design, helping developers choose the right ORM for their projects.
Same Order Table, Two Approaches
Both frameworks define the same Order entity, but MyBatis‑Flex uses @Table and @Id while MyBatis‑Plus uses @TableName and @TableId. The annotation names differ, yet the real contrast lies in how query conditions are built.
Condition Query
MyBatis‑Plus builds a condition with LambdaQueryWrapper<Order>:
LambdaQueryWrapper<Order> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(Order::getUserId, userId)
.eq(Order::getStatus, 1)
.orderByDesc(Order::getCreateTime);
List<Order> orders = orderMapper.selectList(wrapper);MyBatis‑Flex uses a QueryWrapper that references compile‑time generated constants:
QueryWrapper query = QueryWrapper.create()
.where(ORDER.USER_ID.eq(userId))
.and(ORDER.STATUS.eq(1))
.orderBy(ORDER.CREATE_TIME.desc());
List<Order> orders = orderMapper.selectListByQuery(query);The ORDER class is generated by APT; it provides type‑safe field constants that cause a compile‑time error if a column name is misspelled.
Pagination Query
MyBatis‑Plus requires a pagination interceptor and a Page<Order> object:
// configure MybatisPlusInterceptor + PaginationInnerInterceptor first
Page<Order> page = new Page<>(1, 10);
orderMapper.selectPage(page, wrapper);MyBatis‑Flex has pagination built into the core; a single call suffices:
Page<Order> page = orderMapper.paginate(1, 10, query);Architectural Core Differences
MyBatis‑Plus: Startup‑time Injection
During MyBatis startup, AbstractSqlInjector injects a full set of CRUD MappedStatement objects into each mapper. Each operation (Insert, DeleteById, SelectList, etc.) has a dedicated class extending AbstractMethod. The SQL templates are pre‑compiled and registered in MyBatis’ Configuration, ready for runtime use. The downside is that every mapper receives the entire CRUD suite regardless of actual usage, increasing startup cost.
MyBatis‑Flex: Provider Annotations
MyBatis‑Flex relies on native MyBatis annotations such as @SelectProvider and @InsertProvider that point to an EntitySqlProvider. SQL is generated at runtime by the provider based on entity metadata, eliminating the need for pre‑generated statements or interceptors.
The framework promotes three "light" principles:
Light Dependency : only depends on MyBatis.
Light Implementation : no interceptors; features like pagination are built into the core.
Light Runtime : no SQL parsing; SQL is assembled directly.
APT: Compile‑time Code Generation
During mvn compile, the mybatis-flex-processor scans entities annotated with @Table and generates two artifacts:
A TableDef class (e.g., OrderTableDef) containing QueryColumn constants such as ORDER.USER_ID.
A mapper interface that extends BaseMapper if the project does not provide one.
This yields compile‑time type safety: using a wrong column name causes a compilation error. The trade‑off is the learning curve required to understand the generated ORDER class.
Multi‑table Query: Biggest Difference
For a join query, MyBatis‑Plus forces XML mapping:
<select id="listWithDetail" resultType="Order">
SELECT o.*, d.product_name, d.price, d.quantity
FROM `order` o
LEFT JOIN order_detail d ON o.id = d.order_id
WHERE o.status = 1
</select>MyBatis‑Flex can express the same join directly in Java:
QueryWrapper query = QueryWrapper.create()
.select()
.from(ORDER)
.leftJoin(ORDER_DETAIL).on(ORDER.ID.eq(ORDER_DETAIL.ORDER_ID))
.where(ORDER.STATUS.eq(1));
List<Order> orders = orderMapper.selectListByQuery(query);No XML, no extra mapper method, and the join fields are compile‑time constants, guaranteeing type safety.
QueryWrapper Design Differences
MyBatis‑Plus’ QueryWrapper<T> supports two styles: string field names (prone to typos) and lambda method references (require getters). MyBatis‑Flex’ QueryWrapper is not generic; it builds conditions with APT‑generated QueryColumn constants, offering inherent type safety without getters.
Flex’s wrapper is also serializable, enabling RPC transmission, whereas Plus’s lambda‑based wrapper cannot be serialized.
When a condition value is null, Flex automatically ignores the clause; Plus requires an explicit check such as wrapper.eq(value != null, "column", value).
Partial Field Update
MyBatis‑Plus uses UpdateWrapper to specify each column to update:
UpdateWrapper<Order> wrapper = new UpdateWrapper<>();
wrapper.eq("id", orderId)
.set("status", 2)
.set("total_amount", new BigDecimal("0.00"));
orderMapper.update(null, wrapper);MyBatis‑Flex provides UpdateEntity.of(), which records only the fields whose setters are called:
Order order = UpdateEntity.of(Order.class, orderId);
order.setStatus(2);
order.setTotalAmount(new BigDecimal("0.00"));
orderMapper.update(order);This design also allows setting a field to null without a second update call.
Db + Row: No‑Entity Operations
Flex offers a Db utility together with Row (a HashMap subclass) to operate without entity classes:
Row row = new Row();
row.set("order_no", "ORD20250703002");
row.set("user_id", 1004L);
row.set("total_amount", new BigDecimal("66.00"));
row.set("status", 0);
Db.insert("`order`", row);This is handy for scripts, data migration, or dynamic tables.
Feature Comparison
SQL generation: startup injection (Plus) vs runtime Provider (Flex).
Interceptors: required for pagination, tenant, etc. in Plus; none in Flex.
SQL parsing: Plus parses original SQL; Flex assembles SQL directly.
Third‑party dependencies: Plus adds core+extension+starter; Flex only depends on MyBatis.
Type‑safe condition building: LambdaQueryWrapper (Plus) vs APT‑generated QueryColumn (Flex).
Null handling: manual in Plus; automatic in Flex.
Pagination: interceptor‑based (Plus) vs built‑in (Flex).
Multi‑table join: XML required in Plus; direct leftJoin in Flex.
QueryWrapper serialization: unsupported in Plus; supported in Flex for RPC.
No‑entity CRUD: unsupported in Plus; provided by Db + Row in Flex.
Partial field update: UpdateWrapper.set() (Plus) vs UpdateEntity.of() (Flex).
Composite primary keys: not supported in Plus; supported in Flex.
Data‑masking/encryption: paid feature in Plus; free in Flex.
Community: mature and large for Plus; newer and smaller for Flex.
Learning curve: low for Plus; requires understanding APT for Flex.
Conclusion
The two frameworks serve different design philosophies. MyBatis‑Plus extends MyBatis heavily, offering a rich feature set, mature ecosystem, and low learning cost, at the expense of larger size, interceptor complexity, and XML‑backed multi‑table queries. MyBatis‑Flex pursues a minimalist approach: no interceptors, no SQL parsing, zero third‑party dependencies, and a type‑safe, join‑capable QueryWrapper. Its trade‑offs are a smaller community and a steeper onboarding curve due to APT‑generated code. Existing projects on MyBatis‑Plus need not switch, but new projects that can invest in learning APT may benefit from Flex, especially when multi‑table queries are frequent.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
samdeepthink
Knowledge Planet: Old Dock's Tech Chronicles Zhihu: SamDeepThinking A technical manager who still codes heavily on the front line. From junior developer to tech lead, then tech manager, now leading the whole front‑ and back‑end development team—leveling up along the way. I have some insights on programming, career development, and tech management.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
