+ implements UserRepository {
+ // 写操作 → MyBatis Plus Delegate(BASE)
+ // 读操作 → Elasticsearch Delegate(READ)
+ // 读失败自动回退到 BASE 代理
+}
+```
+
+## 字段类型映射
+
+| FieldType | Java 类型 | Elasticsearch 类型 |
+|-----------|----------|-------------------|
+| STRING | String | keyword / text |
+| INTEGER | Integer | integer |
+| LONG | Long | long |
+| DECIMAL | BigDecimal | double |
+| BOOLEAN | Boolean | boolean |
+| DATE | LocalDate | date |
+| DATETIME | LocalDateTime | date |
+| TEXT | String | text |
+
+> **注**:低代码模式下未创建显式 mapping,使用 ES 动态映射;如需精确控制类型,请通过类型化仓储 + `@Document` / `@Field` 注解定义 PO。
+
+## 注意事项
+
+1. **索引创建**:低代码初始化时若索引不存在会自动创建,已存在则跳过;不创建显式 mapping,依赖 ES 动态映射
+2. **更新策略**:`doUpdate` 采用 "先删除后索引" 方式实现,并非部分更新(`UpdateQuery`),可能导致瞬时不可查
+3. **ID 类型**:ES 文档 ID 统一为 `String`,所有 ID 通过 `String.valueOf()` 转换
+4. **条件查询**:当前仅支持等值查询(`Criteria.is`),暂不支持范围、全文检索等复杂查询;如需复杂搜索请自定义 Delegate
+5. **批量操作**:`saveBatch` / `removeBatchByIds` / `listByIds` 均为循环单条操作,未使用 `bulk` API,大批量场景需评估性能
+6. **分页排序**:默认使用 `Sort.unsorted()`,暂未支持通过 `ReqPage` 传递排序字段
+7. **深度分页**:当前使用 `PageRequest` from/size 分页,超过 10000 条需通过 `search_after` 或 `scroll` API,建议业务侧限制
+8. **全量查询**:`queryList(null)` 与 `queryPage` 使用 `Criteria.where("*").exists()` 或 `Criteria.where("_id").exists()` 匹配全部,性能取决于索引规模
+9. **刷新策略**:ES 默认 1 秒刷新,写后立即读可能查不到,如需立即读到可通过 `RefreshPolicy.IMMEDIATE` 配置 `ElasticsearchOperations`
+10. **PO 复用**:与其他 starter 共享同一 PO 时,需注意 `@Document` 等 ES 注解在非 ES 环境下应可被忽略
+
+## 许可证
+
+本项目遵循 Apache License 2.0
diff --git a/structure-infra-elasticsearch-starter/src/main/java/cn/structure/infra/elasticsearch/lowcode/ElasticsearchLowCodeRepoFactory.java b/structure-infra-elasticsearch-starter/src/main/java/cn/structure/infra/elasticsearch/lowcode/ElasticsearchLowCodeRepoFactory.java
index 6f1bb4b..e3cb83f 100644
--- a/structure-infra-elasticsearch-starter/src/main/java/cn/structure/infra/elasticsearch/lowcode/ElasticsearchLowCodeRepoFactory.java
+++ b/structure-infra-elasticsearch-starter/src/main/java/cn/structure/infra/elasticsearch/lowcode/ElasticsearchLowCodeRepoFactory.java
@@ -36,11 +36,25 @@ public ElasticsearchLowCodeRepoFactory(ElasticsearchOperations elasticsearchOper
this.elasticsearchOperations = elasticsearchOperations;
}
+ /**
+ * 返回该工厂支持的存储类型,用于低代码路由引擎匹配。
+ *
+ * @return 固定返回 {@link StorageType#ELASTICSEARCH}
+ */
@Override
public StorageType getType() {
return StorageType.ELASTICSEARCH;
}
+ /**
+ * 创建 Elasticsearch 低代码存储实例。
+ *
+ * 内部构造 {@link ElasticsearchLowCodeStorage},由其在初始化时自动创建索引(不创建 mapping)。
+ *
+ * @param schema 资源 schema 定义(索引名、字段、主键等)
+ * @param config 仓储配置(当前实现未使用,保留以匹配 SPI 签名)
+ * @return 低代码存储实例
+ */
@Override
public LowCodeStorage createStorage(ResourceSchema schema, RepositoryConfig config) {
return new ElasticsearchLowCodeStorage(schema, elasticsearchOperations);
diff --git a/structure-infra-elasticsearch-starter/src/main/java/cn/structure/infra/elasticsearch/lowcode/ElasticsearchLowCodeStorage.java b/structure-infra-elasticsearch-starter/src/main/java/cn/structure/infra/elasticsearch/lowcode/ElasticsearchLowCodeStorage.java
index 5a5150a..62d1f2c 100644
--- a/structure-infra-elasticsearch-starter/src/main/java/cn/structure/infra/elasticsearch/lowcode/ElasticsearchLowCodeStorage.java
+++ b/structure-infra-elasticsearch-starter/src/main/java/cn/structure/infra/elasticsearch/lowcode/ElasticsearchLowCodeStorage.java
@@ -29,15 +29,19 @@
/**
* Elasticsearch 低代码仓储实现
*
- * 基于 Spring Data Elasticsearch 的低代码存储实现,使用 Map 代替 POJO 操作文档。
+ * 基于 Spring Data Elasticsearch 的低代码存储实现,使用 {@code Map} 代替 POJO 操作文档,
+ * 通过 {@link ElasticsearchOperations} 动态操作 ES 索引。
*
* 核心特性:
*
* - Map 动态操作:使用 Map 代替 POJO,无需定义实体类
- * - 自动创建索引:初始化时自动创建索引
+ * - 自动创建索引:初始化时自动创建索引(不创建 mapping,由 ES 动态映射字段类型)
+ * - 更新策略:先 delete 再 index(非部分更新),保证文档状态与入参一致
+ * - 索引定位:通过 {@link IndexCoordinates#of(String)} 以 schema.tableName 定位索引
* - 自动填充:支持创建时间、更新时间自动填充
* - 动态查询:根据查询条件动态构建 Elasticsearch 查询
* - 分页查询:支持分页查询,自动处理总数统计
+ * - 常作为 CQRS 读侧:适用于全文检索、聚合分析等读多写少场景
*
*
* @author chuck
@@ -47,8 +51,11 @@
@Slf4j
public class ElasticsearchLowCodeStorage implements LowCodeStorage {
+ /** 资源 schema 定义(索引名、字段、主键等) */
private final ResourceSchema schema;
+ /** Elasticsearch 操作模板 */
private final ElasticsearchOperations elasticsearchOperations;
+ /** 索引坐标,由 schema.tableName 构建,用于定位 ES 索引 */
private final IndexCoordinates indexCoordinates;
/**
@@ -60,14 +67,21 @@ public class ElasticsearchLowCodeStorage implements LowCodeStorage {
public ElasticsearchLowCodeStorage(ResourceSchema schema, ElasticsearchOperations elasticsearchOperations) {
this.schema = schema;
this.elasticsearchOperations = elasticsearchOperations;
+ // 通过 schema.tableName 构建 IndexCoordinates,后续所有操作均以此定位索引
this.indexCoordinates = IndexCoordinates.of(schema.getTableName());
}
+ /**
+ * 初始化存储结构:自动创建 ES 索引(不创建 mapping)。
+ *
+ * 若索引不存在则调用 {@code indexOps.create()} 创建空索引,由 ES 在首次写入时动态映射字段类型;
+ * 已存在则跳过。
+ */
@Override
public void initialize() {
String indexName = schema.getTableName();
- // 检查索引是否存在,不存在则创建
+ // 检查索引是否存在,不存在则创建(不创建 mapping,由 ES 动态映射)
boolean indexExists = elasticsearchOperations.indexOps(indexCoordinates).exists();
if (!indexExists) {
elasticsearchOperations.indexOps(indexCoordinates).create();
@@ -77,17 +91,33 @@ public void initialize() {
log.info("Elasticsearch lowcode storage initialized: {}", indexName);
}
+ /**
+ * 保存或更新一条记录(以 Map 形式)。
+ *
+ * 处理流程:
+ *
+ * - 拷贝入参 Map,避免污染调用方
+ * - 按 {@link AutoFillType#CREATE} 与 {@link AutoFillType#CREATE_UPDATE} 自动填充时间字段
+ * - 根据主键是否存在且库中已有同 ID 文档,决定走 doUpdate 或 doIndex
+ *
+ *
+ * @param data 数据 Map,键为字段名、值为字段值
+ * @return 保存后的数据(含自动填充字段)
+ */
@Override
public Map save(Map data) {
+ // 拷贝一份,避免污染调用方传入的 Map
Map rowData = new HashMap<>(data);
+ // 创建场景填充:CREATE_TIME 等
fillAutoFields(rowData, AutoFillType.CREATE);
+ // 创建/更新双重填充:CREATE_UPDATE 字段
fillAutoFields(rowData, AutoFillType.CREATE_UPDATE);
String idField = schema.getIdFieldName();
Object idValue = rowData.get(idField);
if (idValue != null) {
- // 更新操作 - 先检查是否存在
+ // 已带主键时先查库,存在则更新、不存在则插入
Map existing = findById(idValue);
if (existing != null) {
return doUpdate(rowData);
@@ -98,20 +128,24 @@ public Map save(Map data) {
}
/**
- * 执行索引操作(新增或更新)
+ * 执行索引操作(新增或覆盖索引)。
+ *
+ * 通过 {@link IndexQueryBuilder} 构建索引请求,主键值统一转换为 String 作为 ES 文档 ID。
*
* @param data 数据
- * @return 索引后的数据
+ * @return 索引后的数据(含 ES 返回的文档 ID)
*/
private Map doIndex(Map data) {
String idField = schema.getIdFieldName();
Object idValue = data.get(idField);
+ // ID 统一转换为 String 作为 ES 文档 ID
IndexQuery indexQuery = new IndexQueryBuilder()
.withId(idValue != null ? String.valueOf(idValue) : null)
.withObject(data)
.build();
+ // 执行索引操作,返回 ES 文档 ID
String documentId = elasticsearchOperations.index(indexQuery, indexCoordinates);
data.put(idField, documentId);
@@ -119,7 +153,10 @@ private Map doIndex(Map data) {
}
/**
- * 执行更新操作
+ * 执行更新操作。
+ *
+ * 采用"先 delete 再 index"策略(非部分更新):先按 ID 删除旧文档,再以入参完整索引新文档,
+ * 保证文档状态与入参一致,避免部分更新遗漏字段。
*
* @param data 数据
* @return 更新后的数据
@@ -128,46 +165,91 @@ private Map doUpdate(Map data) {
String idField = schema.getIdFieldName();
Object idValue = data.get(idField);
- // 删除旧文档
+ // 步骤1:删除旧文档(ID 转 String)
elasticsearchOperations.delete(String.valueOf(idValue), indexCoordinates);
- // 重新索引
+ // 步骤2:以入参完整重新索引(非部分更新)
return doIndex(data);
}
+ /**
+ * 根据主键删除文档。
+ *
+ * ID 统一转换为 String 作为 ES 文档 ID。
+ *
+ * @param id 主键值
+ */
@Override
public void removeById(Object id) {
elasticsearchOperations.delete(String.valueOf(id), indexCoordinates);
}
+ /**
+ * 根据主键查询单条记录。
+ *
+ * ID 统一转换为 String 作为 ES 文档 ID,结果以 Map 形式返回。
+ *
+ * @param id 主键值
+ * @return 数据 Map,未找到时返回 null
+ */
@Override
+ @SuppressWarnings({"unchecked", "rawtypes"})
public Map findById(Object id) {
Map result = elasticsearchOperations.get(String.valueOf(id), Map.class, indexCoordinates);
return result;
}
+ /**
+ * 根据主键查询(与 findById 等价,语义上用于"读模型",常作为 CQRS 读侧)。
+ *
+ * @param id 主键值
+ * @return 数据 Map,未找到时返回 null
+ */
@Override
public Map queryById(Object id) {
return findById(id);
}
+ /**
+ * 根据条件查询单条记录,取结果集首条。
+ *
+ * @param queryParams 查询条件 Map,键为字段名、值为等值匹配值
+ * @return 首条匹配记录,无匹配时返回 null
+ */
@Override
public Map queryOne(Map queryParams) {
List