feat: 增加关联信息

This commit is contained in:
wangyu 2024-06-27 18:03:05 +08:00
parent 873635d6a9
commit 0f27376a5f
16 changed files with 341 additions and 51 deletions

View File

@ -6,6 +6,7 @@ import com.flyfish.framework.query.Query;
import com.flyfish.framework.query.QueryDefinition;
import com.flyfish.framework.utils.CopyUtils;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Example;
import org.springframework.data.domain.Pageable;
@ -35,6 +36,7 @@ public class BaseQo<T extends Domain> implements Qo<T> {
protected List<String> fields;
@Getter
@Setter
protected boolean fetchRef;
public Qo<T> accept(List<T> result, Pageable pageable) {

View File

@ -0,0 +1,32 @@
package com.flyfish.framework.annotations.relation;
import org.springframework.data.annotation.Reference;
import org.springframework.data.annotation.Transient;
import java.lang.annotation.*;
/**
* 数据库关联信息
*
* @author wangyu
*/
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Transient
@Reference
public @interface Association {
/**
* @return 关联在本对象上的字段名称
* 如当前表的detail_id字段对应detail表的id字段则该值为 detail_id
*/
String field() default "";
/**
* @return 关联在外部对象上的字段名称注意禁止循环依赖
* 如当前表不存在目标表detail的关联id目标表关联本实体的id为main_id则该值为main_id
* 与field互斥同时指定永远都以field为主
*/
String foreignField() default "";
}

View File

@ -16,6 +16,11 @@ import java.util.Optional;
*/
public interface Qo<T> {
/**
* 查询键
*/
String KEY = "query";
/**
* 获取分页对象
*

View File

@ -2,6 +2,8 @@ package com.flyfish.framework.r2dbc.config;
import com.flyfish.framework.domain.base.Domain;
import com.flyfish.framework.r2dbc.config.callback.ReferenceR2dbcCallback;
import com.flyfish.framework.r2dbc.metadata.R2dbcMetadataManager;
import com.flyfish.framework.r2dbc.metadata.impl.SimpleR2dbcMetadataManager;
import com.flyfish.framework.r2dbc.operations.R2dbcReactiveEntityOperations;
import com.flyfish.framework.r2dbc.repository.factory.DefaultReactiveRepositoryFactoryBean;
import com.flyfish.framework.r2dbc.repository.impl.DefaultReactiveRepositoryImpl;
@ -42,8 +44,13 @@ public class R2dbcDataConfig {
}
@Bean
public EntityCallback<Domain> referenceR2dbcCallback() {
return new ReferenceR2dbcCallback();
public EntityCallback<Domain> referenceR2dbcCallback(R2dbcMetadataManager metadataManager) {
return new ReferenceR2dbcCallback(metadataManager);
}
@Bean
public R2dbcMetadataManager r2dbcMetadataManager() {
return new SimpleR2dbcMetadataManager();
}
/**

View File

@ -1,32 +1,66 @@
package com.flyfish.framework.r2dbc.config.callback;
import com.flyfish.framework.domain.base.Domain;
import lombok.Setter;
import com.flyfish.framework.domain.base.Qo;
import com.flyfish.framework.r2dbc.metadata.R2dbcMetadataManager;
import com.flyfish.framework.r2dbc.metadata.R2dbcTableMetadata;
import com.flyfish.framework.repository.DefaultReactiveRepository;
import lombok.RequiredArgsConstructor;
import org.apache.commons.collections4.CollectionUtils;
import org.reactivestreams.Publisher;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.data.mapping.PersistentEntity;
import org.springframework.data.r2dbc.core.R2dbcEntityOperations;
import org.springframework.data.r2dbc.mapping.event.AfterConvertCallback;
import org.springframework.data.relational.core.sql.SqlIdentifier;
import org.springframework.data.util.Lazy;
import org.springframework.lang.NonNull;
import reactor.core.publisher.Mono;
public class ReferenceR2dbcCallback implements AfterConvertCallback<Domain>, ApplicationContextAware {
import java.util.Map;
import java.util.Optional;
import java.util.function.Function;
import java.util.stream.Collectors;
@Setter
private ApplicationContext applicationContext;
@RequiredArgsConstructor
public class ReferenceR2dbcCallback implements AfterConvertCallback<Domain> {
@Setter(onMethod_ = @Autowired)
private ObjectProvider<R2dbcEntityOperations> entityOperations;
private final R2dbcMetadataManager r2dbcMetadataManager;
private Lazy<Map<Class<?>, DefaultReactiveRepository<?>>> repositories;
@Override
@NonNull
public Publisher<Domain> onAfterConvert(@NonNull Domain entity, @NonNull SqlIdentifier table) {
return Mono.deferContextual(ctx -> {
Optional<Qo<? extends Domain>> query = ctx.getOrEmpty(Qo.KEY);
query.ifPresent(qo -> entity.setCurrentUser(qo.getUser()));
// 判断是否需要获取关联信息
boolean fetchRefs = query.map(Qo::isFetchRef).orElse(false);
if (fetchRefs) {
return doFetch(entity);
}
return Mono.just(entity);
});
}
/**
* 完成填充
*
* @param entity 实体
* @return 结果
*/
private Mono<Domain> doFetch(Domain entity) {
// 遍历fields找到要注入的数据
entityOperations.getIfAvailable();
R2dbcTableMetadata metadata = r2dbcMetadataManager.getMetadata(entity.getClass());
// 尝试填充关联
if (CollectionUtils.isNotEmpty(metadata.getAssociations())) {
}
return Mono.just(entity);
}
@Autowired
public void setRepositories(ObjectProvider<DefaultReactiveRepository<?>> repositories) {
this.repositories = Lazy.of(() -> repositories.stream()
.collect(Collectors.toMap(repo -> repo.getEntityInformation().getJavaType(), Function.identity())));
}
}

View File

@ -0,0 +1,19 @@
package com.flyfish.framework.r2dbc.metadata;
import com.flyfish.framework.domain.base.Domain;
/**
* r2dbc元数据管理器
*
* @author wangyu
*/
public interface R2dbcMetadataManager {
/**
* 获取元数据
*
* @param entityClass 实体类名
* @return 结果
*/
R2dbcTableMetadata getMetadata(Class<? extends Domain> entityClass);
}

View File

@ -0,0 +1,41 @@
package com.flyfish.framework.r2dbc.metadata;
import com.flyfish.framework.annotations.Property;
import com.flyfish.framework.domain.base.Domain;
import com.flyfish.framework.r2dbc.metadata.reference.R2dbcAssociation;
import com.flyfish.framework.r2dbc.metadata.reference.R2dbcCollection;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;
/**
* 表元数据
*
* @author wangyu
*/
@Data
@Property("表的元数据,用于关联查询")
public class R2dbcTableMetadata {
@Property("表名")
private String tableName;
@Property("表实体类名")
private Class<? extends Domain> entityClass;
@Property("一对一关联信息")
private List<R2dbcAssociation> associations = new ArrayList<>();
@Property("一对多关联信息")
private List<R2dbcCollection> collections = new ArrayList<>();
public R2dbcTableMetadata(Class<? extends Domain> entityClass) {
this.entityClass = entityClass;
}
public void addAssociation(String field, boolean inner) {
this.associations.add(R2dbcAssociation.of(entityClass, field, inner));
}
}

View File

@ -0,0 +1,106 @@
package com.flyfish.framework.r2dbc.metadata.impl;
import com.flyfish.framework.annotations.relation.Association;
import com.flyfish.framework.domain.base.Domain;
import com.flyfish.framework.r2dbc.metadata.R2dbcMetadataManager;
import com.flyfish.framework.r2dbc.metadata.R2dbcTableMetadata;
import com.flyfish.framework.r2dbc.metadata.reference.R2dbcAssociation;
import org.apache.commons.lang3.ClassUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.data.mapping.model.Property;
import org.springframework.data.util.ClassTypeInformation;
import org.springframework.lang.NonNull;
import org.springframework.util.ReflectionUtils;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.Collection;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.function.Function;
import java.util.stream.Collectors;
/**
* 简单的元数据管理器
*
* @author wangyu
* 基于线程安全的反射方法进行加载并持有
*/
public class SimpleR2dbcMetadataManager implements R2dbcMetadataManager {
private final ConcurrentMap<Class<? extends Domain>, R2dbcTableMetadata> METADATA_STORE = new ConcurrentHashMap<>();
/**
* 获取元数据
*
* @param entityClass 实体类名
* @return 结果
*/
@Override
public R2dbcTableMetadata getMetadata(Class<? extends Domain> entityClass) {
return METADATA_STORE.computeIfAbsent(entityClass, this::computeMetadata);
}
/**
* 计算元数据信息
*
* @param entityClass 实体类
* @return 结果
*/
private R2dbcTableMetadata computeMetadata(Class<? extends Domain> entityClass) {
MetadataCreator creator = new MetadataCreator(entityClass);
ReflectionUtils.doWithFields(entityClass, creator);
return creator.metadata;
}
private static class MetadataCreator implements ReflectionUtils.FieldCallback {
private final Map<String, PropertyDescriptor> descriptors;
private final ClassTypeInformation<? extends Domain> typeInformation;
private final R2dbcTableMetadata metadata;
private MetadataCreator(Class<? extends Domain> entityClass) {
this.descriptors = Arrays.stream(BeanUtils.getPropertyDescriptors(entityClass))
.collect(Collectors.toMap(PropertyDescriptor::getName, Function.identity()));
this.typeInformation = ClassTypeInformation.from(entityClass);
this.metadata = new R2dbcTableMetadata(entityClass);
}
/**
* Perform an operation using the given field.
*
* @param field the field to operate on
*/
@Override
public void doWith(@NonNull Field field) throws IllegalArgumentException, IllegalAccessException {
Property property = Property.of(typeInformation, field, descriptors.get(field.getName()));
Class<?> fieldType = property.getType();
ReflectionUtils.makeAccessible(field);
Association association = AnnotatedElementUtils.findMergedAnnotation(field, Association.class);
if (null != association) {
if (ClassUtils.isAssignable(fieldType, Domain.class)) {
// 一对一关联
if (StringUtils.isNotBlank(association.field())) {
metadata.addAssociation(association.field(), true);
} else if (StringUtils.isNotBlank(association.foreignField())) {
metadata.addAssociation(association.foreignField(), false);
} else {
// 尚且不支持空白策略
}
} else if (ClassUtils.isAssignable(fieldType, Collection.class)) {
// 一对多关联
}
}
}
}
}

View File

@ -0,0 +1,25 @@
package com.flyfish.framework.r2dbc.metadata.reference;
import com.flyfish.framework.annotations.Property;
import com.flyfish.framework.domain.base.Domain;
import lombok.AllArgsConstructor;
import lombok.Data;
@Data
@Property("一对一关联查询")
@AllArgsConstructor
public class R2dbcAssociation {
@Property("关联实体类")
private Class<? extends Domain> entityClass;
@Property("关联字段")
private String field;
@Property("字段是否在当前实体内部")
boolean inner;
public static R2dbcAssociation of(Class<? extends Domain> entityClass, String field, boolean inner) {
return new R2dbcAssociation(entityClass, field, inner);
}
}

View File

@ -0,0 +1,16 @@
package com.flyfish.framework.r2dbc.metadata.reference;
import com.flyfish.framework.annotations.Property;
import com.flyfish.framework.domain.base.Domain;
import lombok.Data;
@Data
@Property("一对多关联查询")
public class R2dbcCollection {
@Property("关联实体类")
private Class<? extends Domain> entityClass;
@Property("关联字段")
private String field;
}

View File

@ -18,7 +18,6 @@ import org.springframework.data.relational.core.query.Criteria;
import org.springframework.data.relational.core.query.Query;
import org.springframework.data.relational.repository.query.RelationalEntityInformation;
import org.springframework.data.repository.core.EntityInformation;
import org.springframework.data.util.CastUtils;
import org.springframework.data.util.Lazy;
import org.springframework.lang.NonNull;
import org.springframework.util.Assert;
@ -59,12 +58,6 @@ public class DefaultReactiveRepositoryImpl<T extends Domain> extends SimpleR2dbc
.getRequiredPersistentEntity(entity.getJavaType()));
}
@Override
@NonNull
public Mono<T> findById(@NonNull String id) {
return super.findById(id).flatMap(this::afterSelect);
}
/**
* 通过名称查找一个
*
@ -75,8 +68,7 @@ public class DefaultReactiveRepositoryImpl<T extends Domain> extends SimpleR2dbc
public Mono<T> findByName(String name) {
if (StringUtils.isNotBlank(name)) {
return entityOperations
.selectOne(Query.query(Criteria.where("name").is(name)), entity.getJavaType())
.flatMap(this::afterSelect);
.selectOne(Query.query(Criteria.where("name").is(name)), entity.getJavaType());
}
return Mono.empty();
}
@ -93,7 +85,7 @@ public class DefaultReactiveRepositoryImpl<T extends Domain> extends SimpleR2dbc
public Mono<T> findOne(Qo<T> query) {
return getQuery(query)
.flatMap(querying -> entityOperations.selectOne(querying, entity.getJavaType()))
.flatMap(t -> this.afterSelect(query, t));
.contextWrite(ctx -> ctx.put(Qo.KEY, query));
}
/**
@ -106,8 +98,8 @@ public class DefaultReactiveRepositoryImpl<T extends Domain> extends SimpleR2dbc
@Override
public Flux<T> findAll(Qo<T> query) {
return getQuery(query)
.flatMapMany(querying -> entityOperations.select(querying, entity.getJavaType())
.flatMap(t -> this.afterSelect(query, t)));
.flatMapMany(querying -> entityOperations.select(querying, entity.getJavaType()))
.contextWrite(ctx -> ctx.put(Qo.KEY, query));
}
/**
@ -123,8 +115,8 @@ public class DefaultReactiveRepositoryImpl<T extends Domain> extends SimpleR2dbc
@Override
public Flux<T> findAll(Qo<T> query, Sort sort) {
return getQuery(query)
.flatMapMany(querying -> entityOperations.select(querying.sort(sort), entity.getJavaType())
.flatMap(t -> this.afterSelect(query, t)));
.flatMapMany(querying -> entityOperations.select(querying.sort(sort), entity.getJavaType()))
.contextWrite(ctx -> ctx.put(Qo.KEY, query));
}
/**
@ -140,10 +132,10 @@ public class DefaultReactiveRepositoryImpl<T extends Domain> extends SimpleR2dbc
return getQuery(query)
.flatMap(querying -> entityOperations.select(querying.with(pageable),
entity.getJavaType())
.flatMap(t -> this.afterSelect(query, t))
.collectList()
.flatMap(list -> ReactivePageableExecutionUtils.getPage(list, pageable, this.count(query))))
.defaultIfEmpty(Page.empty());
.defaultIfEmpty(Page.empty())
.contextWrite(ctx -> ctx.put(Qo.KEY, query));
}
/**
@ -170,7 +162,7 @@ public class DefaultReactiveRepositoryImpl<T extends Domain> extends SimpleR2dbc
public Flux<T> findAllByValues(String key, List<?> values) {
Criteria criteria = Criteria.where(key).in(values);
Query query = Query.query(criteria);
return entityOperations.select(query, entity.getJavaType()).flatMap(this::afterSelect);
return entityOperations.select(query, entity.getJavaType());
}
/**
@ -247,13 +239,4 @@ public class DefaultReactiveRepositoryImpl<T extends Domain> extends SimpleR2dbc
private Mono<Query> getQuery(Qo<T> qo) {
return Mono.justOrEmpty(qo.getQuery(entity));
}
private Mono<T> afterSelect(T entity) {
return Mono.just(entity);
}
private Mono<T> afterSelect(Qo<T> qo, T entity) {
entity.setCurrentUser(qo.getUser());
return afterSelect(entity);
}
}

View File

@ -4,7 +4,6 @@ import com.flyfish.framework.r2dbc.config.R2dbcDataConfig;
import com.flyfish.framework.r2dbc.repository.TestDO;
import com.flyfish.framework.r2dbc.repository.TestQO;
import com.flyfish.framework.r2dbc.repository.TestRepository;
import com.flyfish.framework.r2dbc.repository.reference.TestAsso;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@ -26,9 +25,12 @@ public class R2DbcRepositoryTest {
@Test
public void test() {
TestQO qo = new TestQO();
qo.setName("");
qo.setName("2");
// 开启关联查询
qo.setFetchRef(true);
TestDO test = new TestDO();
test.setId("1");
test.setId("100");
test.setCode("ttt");
test.setName("测试名称");
test.setOtherId("1");

View File

@ -1,11 +1,10 @@
package com.flyfish.framework.r2dbc.repository;
import com.flyfish.framework.annotations.relation.Association;
import com.flyfish.framework.domain.base.AuditDomain;
import com.flyfish.framework.r2dbc.repository.reference.TestAsso;
import com.flyfish.framework.r2dbc.repository.reference.TestChild;
import lombok.Data;
import org.springframework.data.annotation.Reference;
import org.springframework.data.annotation.Transient;
import org.springframework.data.relational.core.mapping.Column;
import org.springframework.data.relational.core.mapping.Table;
@ -16,10 +15,11 @@ import java.util.List;
public class TestDO extends AuditDomain {
@Column("other_id")
@Reference
private String otherId;
@Reference(TestChild.class)
@Transient
@Association(field = "other_id")
private TestAsso asso;
@Association(foreignField = "parent_id")
private List<TestChild> children;
}

View File

@ -1,5 +1,5 @@
spring:
r2dbc:
url: r2dbc:mysql://127.0.0.1:3306/test?allowMultiQueries=true&useUnicode=true&ssl=false&characterEncoding=UTF-8&serverZoneId=Asia/Shanghai&autoReconnect=true&nullCatalogMeansCurrent=true
url: r2dbc:mysql://127.0.0.1:3306/test?allowMultiQueries=true&useUnicode=true&ssl=true&characterEncoding=UTF-8&serverZoneId=Asia/Shanghai&autoReconnect=true&nullCatalogMeansCurrent=true
username: root
password: Unicom#2018

View File

@ -1,2 +1,3 @@
DROP TABLE IF EXISTS `test`;
DROP TABLE IF EXISTS `test_child`;
DROP TABLE IF EXISTS `test_asso`;

View File

@ -14,6 +14,10 @@ CREATE TABLE IF NOT EXISTS `test`
PRIMARY KEY (`id`)
) COMMENT '测试表';
REPLACE INTO `test`
VALUES ('1', '1', '测试1', '1', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, null, null, null, null, false),
('2', '2', '测试2', '2', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, null, null, null, null, false);
CREATE TABLE IF NOT EXISTS `test_child`
(
`id` VARCHAR(36) NOT NULL COMMENT '主键',
@ -30,6 +34,14 @@ CREATE TABLE IF NOT EXISTS `test_child`
PRIMARY KEY (`id`)
) COMMENT '测试子表';
REPLACE INTO `test_child`
VALUES ('1', '1', '子表1', 1, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, null, null, null, null, false),
('2', '2', '子表2', 1, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, null, null, null, null, false),
('3', '3', '子表3', 1, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, null, null, null, null, false),
('4', '4', '子表4', 1, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, null, null, null, null, false),
('5', '5', '子表5', 1, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, null, null, null, null, false);
CREATE TABLE IF NOT EXISTS `test_asso`
(
`id` VARCHAR(36) NOT NULL COMMENT '主键',
@ -43,4 +55,9 @@ CREATE TABLE IF NOT EXISTS `test_asso`
`modifier_id` VARCHAR(36) NULL COMMENT '修改人id',
`delete` BIT(1) NOT NULL DEFAULT b'0',
PRIMARY KEY (`id`)
) COMMENT '测试关联表';
) COMMENT '测试关联表';
REPLACE INTO `test_asso`
VALUES ('1', '1', 'baba ', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, null, null, null, null, false),
('2', '1', 'mama ', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, null, null, null, null, false);