diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts
index ac662e4d..397957ee 100644
--- a/docs/.vitepress/config.ts
+++ b/docs/.vitepress/config.ts
@@ -58,6 +58,7 @@ export default defineConfig({
items: [
{text: '增删改', link: '/zh/base/add-delete-update'},
{text: '查询和分页', link: '/zh/base/query'},
+ {text: 'IService', link: '/zh/base/service'},
{text: 'QueryWrapper', link: '/zh/base/querywrapper'},
]
},
diff --git a/docs/zh/base/service.md b/docs/zh/base/service.md
new file mode 100644
index 00000000..17b0f7a2
--- /dev/null
+++ b/docs/zh/base/service.md
@@ -0,0 +1,73 @@
+# 顶级 Service 接口
+
+MyBatis-Flex 提供了一个名为 `IService` 的接口,及其默认实现类 `ServiceImpl` ,用于简化在 「Service」 层重复定义 「Mapper」 层的方法。
+
+> `IService` 接口只是提供了简单且常用的 “增删改查” 方法,更多细节以及复杂的业务,还是需要使用 `BaseMapper` 进行处理。
+
+## 保存数据
+
+`IService` 的接口提供了 save、saveOrUpdate、saveBatch 方法,用于保存数据:
+
+- **save(entity)**:保存一条数据,忽略 null 值的字段。
+- **saveOrUpdate(entity)**:保存一条数据,如果数据存在则更新数据。
+- **saveBatch(entities)**:批量保存多条数据。
+- **saveBatch(entities, size)**:批量保存多条数据,按指定数量切分。
+
+
+## 删除数据
+
+`IService` 的接口提供了 remove、removeById、removeByIds、removeByMap 方法,用于删除数据:
+
+- **remove(query)**:根据 `QueryCondition` 或 `QueryWrapper` 构建的条件来删除数据。
+- **removeById(id)**:根据主键删除数据,复合主键需要传入一个数组。
+- **removeByIds(ids)**:根据主键的集合,批量删除多条数据。
+- **removeByMap(map)**:根据 `Map<字段名,值>` 组成的条件删除数据,字段名和值的关系为相等的关系;同时,防止 "不小心" 全表删除数据,Map 的值不允许为 null 或者空数据。
+
+
+## 更新数据
+
+`IService` 的接口提供了 update、updateById、updateByMap 方法,用于更新数据:
+
+- **update(entity, query)**:根据 `QueryCondition` 或 `QueryWrapper` 构建的条件更新数据,实体类可以没有主键(如果有也会被忽略),实体类的 null 属性,会自动被忽略。
+- **updateById(entity)**:根据主键更新数据,要求主键值不能为空,否则会抛出异常;同时,数据为 null 的字段不会更新到数据库。
+- **updateByMap(entity, map)**:根据 `Map<字段名,值>` 组成的条件更新数据,实体类可以没有主键(如果有也会被忽略),实体类的 null 属性,会自动被忽略。
+
+
+## 查询数据
+
+### 查询一条数据
+
+`IService` 的接口提供了 getById、getByIdOpt、getOne、getOneOpt 方法,用于查询一条数据:
+
+- **getById(id)**:根据主键查询数据。
+- **getByIdOpt(id)**:根据主键查询数据,并封装为 `Optional` 返回。
+- **getOne(query)**: 根据 `QueryCondition` 或 `QueryWrapper` 构建的条件查询一条数据。
+- **getOneOpt(query)**: 根据 `QueryCondition` 或 `QueryWrapper` 构建的条件查询一条数据,并封装为 `Optional` 返回。
+
+### 查询多条数据
+
+`IService` 的接口提供了 list、listByIds、listByMap 方法,用于查询多条数据:
+
+- **list()**:查询所有数据。
+- **list(query)**:根据 `QueryCondition` 或 `QueryWrapper` 构建的条件查询多条数据。
+- **listByIds(ids)**:根据主键的集合查询多条数据。
+- **listByMap(map)**:根据 `Map<字段名,值>` 组成的条件查询多条数据。
+
+### 查询数据数量
+
+`IService` 的接口提供了 exists、count 方法,用于查询数据数量;
+
+- **count()**:查询所有数据数量。
+- **count(query)**:根据 `QueryCondition` 或 `QueryWrapper` 构建的条件查询数据数量。
+- **exist(query)**:根据 `QueryCondition` 或 `QueryWrapper` 构建的条件判断数据是否存在。
+
+### 分页查询数据
+
+`IService` 的接口提供了 page 方法,用于分页查询数据:
+
+- **page(page)**:分页查询所有数据。
+- **page(page, query)**:根据 `QueryCondition` 或 `QueryWrapper` 构建的条件分页查询数据。
+
+## 其他方法
+
+- **getBaseMapper()**:获取对应的 `BaseMapper` 接口。
\ No newline at end of file
diff --git a/mybatis-flex-core/src/main/java/com/mybatisflex/core/util/SqlUtil.java b/mybatis-flex-core/src/main/java/com/mybatisflex/core/util/SqlUtil.java
index 0ebabaaf..60fc7870 100644
--- a/mybatis-flex-core/src/main/java/com/mybatisflex/core/util/SqlUtil.java
+++ b/mybatis-flex-core/src/main/java/com/mybatisflex/core/util/SqlUtil.java
@@ -62,4 +62,25 @@ public class SqlUtil {
}
+ /**
+ * 根据数据库响应结果判断数据库操作是否成功。
+ *
+ * @param result 数据库操作返回影响条数
+ * @return {@code true} 操作成功,{@code false} 操作失败。
+ */
+ public static boolean retBool(int result) {
+ return result >= 1;
+ }
+
+ /**
+ * 根据数据库响应结果判断数据库操作是否成功。
+ *
+ * @param result 数据库操作返回影响条数
+ * @return {@code true} 操作成功,{@code false} 操作失败。
+ */
+ public static boolean retBool(long result) {
+ return result >= 1L;
+ }
+
+
}
diff --git a/mybatis-flex-spring/src/main/java/com/mybatisflex/spring/service/IService.java b/mybatis-flex-spring/src/main/java/com/mybatisflex/spring/service/IService.java
new file mode 100644
index 00000000..55e4c14a
--- /dev/null
+++ b/mybatis-flex-spring/src/main/java/com/mybatisflex/spring/service/IService.java
@@ -0,0 +1,395 @@
+/**
+ * Copyright (c) 2022-2023, Mybatis-Flex (fuhai999@gmail.com).
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.mybatisflex.spring.service;
+
+import com.mybatisflex.core.BaseMapper;
+import com.mybatisflex.core.paginate.Page;
+import com.mybatisflex.core.query.QueryCondition;
+import com.mybatisflex.core.query.QueryWrapper;
+import com.mybatisflex.core.util.CollectionUtil;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.io.Serializable;
+import java.util.*;
+
+import static com.mybatisflex.core.util.SqlUtil.retBool;
+
+/**
+ * 由 Mybatis-Flex 提供的顶级增强 Service 接口。
+ *
+ * @author 王帅
+ * @since 2023-05-01
+ * @param 实体类(Entity)类型
+ */
+@SuppressWarnings("unused")
+public interface IService {
+
+ /**
+ * 获取对应实体类(Entity)的基础映射类(BaseMapper)。
+ *
+ * @return 基础映射类(BaseMapper)
+ */
+ BaseMapper getBaseMapper();
+
+ // ===== 保存(增)操作 =====
+
+ /**
+ * 保存实体类对象数据。
+ *
+ * @param entity 实体类对象
+ * @return {@code true} 保存成功,{@code false} 保存失败。
+ * @apiNote 默认调用的是 {@link BaseMapper#insertSelective(Object)} 方法,忽略
+ * {@code null} 字段的数据,使数据库配置的默认值生效。
+ */
+ default boolean save(T entity) {
+ return retBool(getBaseMapper().insertSelective(entity));
+ }
+
+ /**
+ * 保存或者更新实体类对象数据。
+ *
+ * @param entity 实体类对象
+ * @return {@code true} 保存或更新成功,{@code false} 保存或更新失败。
+ * @apiNote 如果实体类对象主键有值,则更新数据,若没有值,则保存数据。
+ */
+ default boolean saveOrUpdate(T entity) {
+ return retBool(getBaseMapper().insertOrUpdate(entity));
+ }
+
+ /**
+ * 批量保存实体类对象数据。
+ *
+ * @param entities 实体类对象
+ * @return {@code true} 保存成功,{@code false} 保存失败。
+ */
+ @Transactional(rollbackFor = Exception.class)
+ default boolean saveBatch(Collection entities) {
+ return retBool(getBaseMapper().insertBatch(new ArrayList<>(entities)));
+ }
+
+ /**
+ * 批量保存实体类对象数据。
+ *
+ * @param entities 实体类对象
+ * @param size 每次保存切分的数量
+ * @return {@code true} 保存成功,{@code false} 保存失败。
+ */
+ @Transactional(rollbackFor = Exception.class)
+ default boolean saveBatch(Collection entities, int size) {
+ return retBool(getBaseMapper().insertBatch(new ArrayList<>(entities), size));
+ }
+
+ // ===== 删除(删)操作 =====
+
+ /**
+ * 根据查询条件删除数据。
+ *
+ * @param query 查询条件
+ * @return {@code true} 删除成功,{@code false} 删除失败。
+ */
+ default boolean remove(QueryWrapper query) {
+ return retBool(getBaseMapper().deleteByQuery(query));
+ }
+
+ /**
+ * 根据查询条件删除数据。
+ *
+ * @param query 查询条件
+ * @return {@code true} 删除成功,{@code false} 删除失败。
+ */
+ default boolean remove(QueryCondition query) {
+ return retBool(getBaseMapper().deleteByCondition(query));
+ }
+
+ /**
+ * 根据数据主键删除数据。
+ *
+ * @param id 数据主键
+ * @return {@code true} 删除成功,{@code false} 删除失败。
+ */
+ default boolean removeById(Serializable id) {
+ return retBool(getBaseMapper().deleteById(id));
+ }
+
+ /**
+ * 根据数据主键批量删除数据。
+ *
+ * @param ids 数据主键
+ * @return {@code true} 删除成功,{@code false} 删除失败。
+ */
+ @Transactional(rollbackFor = Exception.class)
+ default boolean removeByIds(Collection extends Serializable> ids) {
+ if (CollectionUtil.isEmpty(ids)) {
+ return false;
+ }
+ return retBool(getBaseMapper().deleteBatchByIds(ids));
+ }
+
+ /**
+ * 根据 {@link Map} 构建查询条件删除数据。
+ *
+ * @param query 查询条件
+ * @return {@code true} 删除成功,{@code false} 删除失败。
+ */
+ default boolean removeByMap(Map query) {
+ return retBool(getBaseMapper().deleteByMap(query));
+ }
+
+ // ===== 更新(改)操作 =====
+
+ /**
+ * 根据查询条件更新数据。
+ *
+ * @param entity 实体类对象
+ * @param query 查询条件
+ * @return {@code true} 更新成功,{@code false} 更新失败。
+ */
+ default boolean update(T entity, QueryWrapper query) {
+ return retBool(getBaseMapper().updateByQuery(entity, query));
+ }
+
+ /**
+ * 根据查询条件更新数据。
+ *
+ * @param entity 实体类对象
+ * @param query 查询条件
+ * @return {@code true} 更新成功,{@code false} 更新失败。
+ */
+ default boolean update(T entity, QueryCondition query) {
+ return retBool(getBaseMapper().updateByCondition(entity, query));
+ }
+
+ /**
+ * 根据数据主键更新数据。
+ *
+ * @param entity 实体类对象
+ * @return {@code true} 更新成功,{@code false} 更新失败。
+ */
+ default boolean updateById(T entity) {
+ return retBool(getBaseMapper().update(entity));
+ }
+
+ /**
+ * 根据 {@link Map} 构建查询条件更新数据。
+ *
+ * @param entity 实体类对象
+ * @param query 查询条件
+ * @return {@code true} 更新成功,{@code false} 更新失败。
+ */
+ default boolean updateByMap(T entity, Map query) {
+ return retBool(getBaseMapper().updateByMap(entity, query));
+ }
+
+ // ===== 查询(查)操作 =====
+
+ /**
+ * 根据数据主键查询一条数据。
+ *
+ * @param id 数据主键
+ * @return 查询结果数据
+ */
+ default T getById(Serializable id) {
+ return getBaseMapper().selectOneById(id);
+ }
+
+ /**
+ * 根据数据主键查询一条数据。
+ *
+ * @param id 数据主键
+ * @return 查询结果数据
+ * @apiNote 该方法会将查询结果封装为 {@link Optional} 类进行返回,方便链式操作。
+ */
+ default Optional getByIdOpt(Serializable id) {
+ return Optional.ofNullable(getById(id));
+ }
+
+ /**
+ * 根据查询条件查询一条数据。
+ *
+ * @param query 查询条件
+ * @return 查询结果数据
+ */
+ default T getOne(QueryWrapper query) {
+ return getBaseMapper().selectOneByQuery(query);
+ }
+
+ /**
+ * 根据查询条件查询一条数据。
+ *
+ * @param query 查询条件
+ * @return 查询结果数据
+ * @apiNote 该方法会将查询结果封装为 {@link Optional} 类进行返回,方便链式操作。
+ */
+ default Optional getOneOpt(QueryWrapper query) {
+ return Optional.ofNullable(getOne(query));
+ }
+
+ /**
+ * 根据查询条件查询一条数据。
+ *
+ * @param query 查询条件
+ * @return 查询结果数据
+ */
+ default T getOne(QueryCondition query) {
+ return getBaseMapper().selectOneByCondition(query);
+ }
+
+ /**
+ * 根据查询条件查询一条数据。
+ *
+ * @param query 查询条件
+ * @return 查询结果数据
+ * @apiNote 该方法会将查询结果封装为 {@link Optional} 类进行返回,方便链式操作。
+ */
+ default Optional getOneOpt(QueryCondition query) {
+ return Optional.ofNullable(getOne(query));
+ }
+
+ /**
+ * 查询所有数据。
+ *
+ * @return 所有数据
+ */
+ default List list() {
+ return getBaseMapper().selectAll();
+ }
+
+ /**
+ * 根据查询条件查询数据集合。
+ *
+ * @param query 查询条件
+ * @return 数据集合
+ */
+ default List list(QueryWrapper query) {
+ return getBaseMapper().selectListByQuery(query);
+ }
+
+ /**
+ * 根据查询条件查询数据集合。
+ *
+ * @param query 查询条件
+ * @return 数据集合
+ */
+ default List list(QueryCondition query) {
+ return getBaseMapper().selectListByCondition(query);
+ }
+
+ /**
+ * 根据数据主键查询数据集合。
+ *
+ * @param ids 数据主键
+ * @return 数据集合
+ */
+ default List listByIds(Collection extends Serializable> ids) {
+ return getBaseMapper().selectListByIds(ids);
+ }
+
+ /**
+ * 根据 {@link Map} 构建查询条件查询数据集合。
+ *
+ * @param query 查询条件
+ * @return 数据集合
+ */
+ default List listByMap(Map query) {
+ return getBaseMapper().selectListByMap(query);
+ }
+
+ // ===== 数量查询操作 =====
+
+ /**
+ * 根据查询条件判断数据是否存在。
+ *
+ * @param query 查询条件
+ * @return {@code true} 数据存在,{@code false} 数据不存在。
+ */
+ default boolean exists(QueryWrapper query) {
+ return retBool(count(query));
+ }
+
+ /**
+ * 根据查询条件判断数据是否存在。
+ *
+ * @param query 查询条件
+ * @return {@code true} 数据存在,{@code false} 数据不存在。
+ */
+ default boolean exists(QueryCondition query) {
+ return retBool(count(query));
+ }
+
+ /**
+ * 查询所有数据数量。
+ *
+ * @return 所有数据数量
+ */
+ default long count() {
+ return getBaseMapper().selectCountByQuery(QueryWrapper.create());
+ }
+
+ /**
+ * 根据查询条件查询数据数量。
+ *
+ * @param query 查询条件
+ * @return 数据数量
+ */
+ default long count(QueryWrapper query) {
+ return getBaseMapper().selectCountByQuery(query);
+ }
+
+ /**
+ * 根据查询条件查询数据数量。
+ *
+ * @param query 查询条件
+ * @return 数据数量
+ */
+ default long count(QueryCondition query) {
+ return getBaseMapper().selectCountByCondition(query);
+ }
+
+ // ===== 分页查询操作 =====
+
+ /**
+ * 分页查询所有数据。
+ *
+ * @param page 分页对象
+ * @return 分页对象
+ */
+ default Page page(Page page) {
+ return getBaseMapper().paginate(page, QueryWrapper.create());
+ }
+
+ /**
+ * 根据查询条件分页查询数据。
+ *
+ * @param page 分页对象
+ * @param query 查询条件
+ * @return 分页对象
+ */
+ default Page page(Page page, QueryWrapper query) {
+ return getBaseMapper().paginate(page, query);
+ }
+
+ /**
+ * 根据查询条件分页查询数据。
+ *
+ * @param page 分页对象
+ * @param query 查询条件
+ * @return 分页对象
+ */
+ default Page page(Page page, QueryCondition query) {
+ return getBaseMapper().paginate(page, QueryWrapper.create().where(query));
+ }
+
+}
\ No newline at end of file
diff --git a/mybatis-flex-spring/src/main/java/com/mybatisflex/spring/service/impl/ServiceImpl.java b/mybatis-flex-spring/src/main/java/com/mybatisflex/spring/service/impl/ServiceImpl.java
new file mode 100644
index 00000000..e1c537a9
--- /dev/null
+++ b/mybatis-flex-spring/src/main/java/com/mybatisflex/spring/service/impl/ServiceImpl.java
@@ -0,0 +1,41 @@
+/**
+ * Copyright (c) 2022-2023, Mybatis-Flex (fuhai999@gmail.com).
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.mybatisflex.spring.service.impl;
+
+import com.mybatisflex.core.BaseMapper;
+import com.mybatisflex.spring.service.IService;
+import org.springframework.beans.factory.annotation.Autowired;
+
+/**
+ * 由 Mybatis-Flex 提供的顶级增强 Service 接口的默认实现类。
+ *
+ * @author 王帅
+ * @since 2023-05-01
+ * @param 映射类(Mapper)类型
+ * @param 实体类(Entity)类型
+ */
+@SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection")
+public class ServiceImpl, T> implements IService {
+
+ @Autowired
+ protected M baseMapper;
+
+ @Override
+ public BaseMapper getBaseMapper() {
+ return baseMapper;
+ }
+
+}
\ No newline at end of file