Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/architecture/STATIC_REFERENCE_READ_PROJECTION.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,10 @@ private transient String cStatus;
生命周期等额外业务信息仍由领域 facade 显式组合。`PlatformAbilityRuntime` 是平台内部的装配入口,普通业务
service 不应直接依赖它。

静态 `CrudAbility` 的标准 `list(...)` 与 `pageQuery(...)` 会自动批量回填声明的 `@ReferenceLoad`,
因此普通列表和分页业务代码不需要再调用读取门面。列表默认不自动回填 `@ReferencedBy`,避免一对多集合随
分页记录膨胀;单条读取和明确的领域聚合仍按各自读取边界处理。

## 读投影声明

当前模块对外可带出的关联字段声明在 service 上:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -215,15 +215,29 @@ default int deleteBatch(Collection<String> ids, DeletionContext deletionContext)

@PlatformOperation(PlatformAction.QUERY)
default PageResult<T> pageQuery(Criteria criteria, PageRequest pageRequest, Sort... sorts) {
return getDao().pageQuery(activeCriteria(criteria), pageRequest, sorts);
PageResult<T> result = getDao().pageQuery(activeCriteria(criteria), pageRequest, sorts);
populateDeclaredReferenceLoads(result.getRecords());
return result;
}

default List<T> list(Criteria criteria, PageRequest pageRequest, Sort... sorts) {
return getDao().query(activeCriteria(criteria), pageRequest, sorts);
List<T> records = getDao().query(activeCriteria(criteria), pageRequest, sorts);
populateDeclaredReferenceLoads(records);
return records;
}

default List<T> list(Criteria criteria, Sort... sorts) {
return getDao().list(activeCriteria(criteria), sorts);
List<T> records = getDao().list(activeCriteria(criteria), sorts);
populateDeclaredReferenceLoads(records);
return records;
}

/**
* Internal post-read step shared by standard list reads and ability decorators.
* Ordinary services do not call this directly; standard query methods invoke it automatically.
*/
default void populateDeclaredReferenceLoads(Collection<T> records) {
PlatformAbilityDispatcher.populateReferenceLoads(this, records == null ? List.of() : records);
}

default long count(Criteria criteria) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,10 @@ private static <T extends EntityContract> void runReferenceLoadAfterSelect(CrudA
referenceLoadResolver.populate(ability, entity);
}

static void populateReferenceLoads(CrudAbility<?> ability, java.util.Collection<? extends EntityContract> entities) {
referenceLoadResolver.populateAll(ability, entities);
}

@SuppressWarnings({"rawtypes", "unchecked"})
private static <T extends EntityContract> void runReferenceIntegrityValidation(CrudAbility<T> ability,
T existing,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,21 +39,26 @@ default PageResult<T> pageQuery(Criteria criteria, PageRequest pageRequest, Sort
List<T> records = page.getRecords().stream()
.peek(this::restoreProtectedFieldsFromStorage)
.toList();
populateDeclaredReferenceLoads(records);
return PageResult.of(records, page.getTotal(), pageRequest);
}

@Override
default List<T> list(Criteria criteria, PageRequest pageRequest, Sort... sorts) {
return getDao().query(activeCriteria(criteria), pageRequest, sorts).stream()
List<T> records = getDao().query(activeCriteria(criteria), pageRequest, sorts).stream()
.peek(this::restoreProtectedFieldsFromStorage)
.toList();
populateDeclaredReferenceLoads(records);
return records;
}

@Override
default List<T> list(Criteria criteria, Sort... sorts) {
return getDao().list(activeCriteria(criteria), sorts).stream()
List<T> records = getDao().list(activeCriteria(criteria), sorts).stream()
.peek(this::restoreProtectedFieldsFromStorage)
.toList();
populateDeclaredReferenceLoads(records);
return records;
}

default FieldProtectionMutation protectFieldsForStorage(T entity) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import net.ximatai.muyun.spring.ability.child.ChildrenAbility;
import net.ximatai.muyun.spring.ability.reference.ReferenceDependencyRegistryTestAccess;
import net.ximatai.muyun.spring.ability.reference.ReferenceOption;
import net.ximatai.muyun.spring.ability.reference.ReferenceLoadResolver;
import net.ximatai.muyun.spring.ability.reference.ReferenceTarget;
import net.ximatai.muyun.spring.ability.reference.ReferenceTo;
import net.ximatai.muyun.spring.ability.reference.ReferencerAbility;
Expand Down Expand Up @@ -1848,6 +1849,39 @@ void pageQueryShouldHideSoftDeletedRows() {
.containsExactly(activeId, nullDeletedId);
}

@Test
void listReadsShouldBatchPopulateReferenceLoadsWithoutLoadingInverseCollections() {
StandardDemoBusinessService service = new StandardDemoBusinessService();
String firstId = service.insert(new DemoPlainRecord("First"));
String secondId = service.insert(new DemoPlainRecord("Second"));
List<List<String>> batches = new ArrayList<>();
PlatformAbilityRuntime.configureReferenceLoadResolver(new ReferenceLoadResolver() {
@Override
public void populate(CrudAbility<?> ability, EntityContract entity) {
throw new AssertionError("list reads must use the batch reference-load entry point");
}

@Override
public void populateAll(CrudAbility<?> ability, java.util.Collection<? extends EntityContract> entities) {
batches.add(entities.stream().map(EntityContract::getId).toList());
}
});
PlatformAbilityRuntime.configureReferencedByResolver((ability, entity) -> {
throw new AssertionError("ordinary list reads must not populate inverse collections");
});

assertThat(service.list(Criteria.of())).extracting(DemoPlainRecord::getId)
.containsExactly(firstId, secondId);
assertThat(service.list(Criteria.of(), PageRequest.of(1, 10))).extracting(DemoPlainRecord::getId)
.containsExactly(firstId, secondId);
assertThat(service.pageQuery(Criteria.of(), PageRequest.of(1, 10)).getRecords())
.extracting(DemoPlainRecord::getId)
.containsExactly(firstId, secondId);

assertThat(batches).containsExactly(List.of(firstId, secondId), List.of(firstId, secondId),
List.of(firstId, secondId));
}

@Test
void abilityQueriesShouldNotMutateCallerCriteria() {
DemoOrganizationService service = new DemoOrganizationService();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -140,24 +140,33 @@ void shouldPopulateDynamicReferencedByCollection() {
.referencedBys(List.of(new EntityReferencedByDefinition("classroom", "member", "classroomId", "members")))
.build();
new ModuleDefinitionValidator().validate(module);
DynamicRecord classroomRecord = new DynamicRecord(classroom);
classroomRecord.setId("classroom-1");
DynamicRecord memberRecord = new DynamicRecord(member);
memberRecord.setId("member-1");
memberRecord.setValue("classroomId", "classroom-1");
DynamicRecord firstClassroom = new DynamicRecord(classroom);
firstClassroom.setId("classroom-1");
DynamicRecord secondClassroom = new DynamicRecord(classroom);
secondClassroom.setId("classroom-2");
DynamicRecord firstMember = new DynamicRecord(member);
firstMember.setId("member-1");
firstMember.setValue("classroomId", "classroom-1");
DynamicRecord secondMember = new DynamicRecord(member);
secondMember.setId("member-2");
secondMember.setValue("classroomId", "classroom-2");
DynamicRecordDao classroomDao = mock(DynamicRecordDao.class);
when(classroomDao.getEntity()).thenReturn(classroom);
when(classroomDao.query(any(Criteria.class), any(PageRequest.class), any(Sort[].class))).thenReturn(List.of(classroomRecord));
when(classroomDao.query(any(Criteria.class), any(PageRequest.class), any(Sort[].class)))
.thenReturn(List.of(firstClassroom, secondClassroom));
DynamicEntityService memberService = mock(DynamicEntityService.class);
when(memberService.list(any(Criteria.class))).thenReturn(List.of(memberRecord));
when(memberService.list(any(Criteria.class))).thenReturn(List.of(firstMember, secondMember));

DynamicEntityService service = new DynamicEntityService(classroomDao, "education.school", DynamicRecordLifecycle.NONE,
module, alias -> "member".equals(alias) ? memberService : null,
ignored -> { throw new IllegalStateException("targets are not read"); }, null, DynamicFieldValueValidator.NONE,
FieldCryptoProvider.UNAVAILABLE, FieldSigner.UNAVAILABLE, new PlatformTimeService());

assertThat((List<Object>) service.list(Criteria.of(), PageRequest.of(1, 20)).getFirst().getValue("members"))
.containsExactly(memberRecord);
List<DynamicRecord> classrooms = service.list(Criteria.of(), PageRequest.of(1, 20));

assertThat((List<Object>) classrooms.getFirst().getValue("members")).containsExactly(firstMember);
assertThat((List<Object>) classrooms.get(1).getValue("members")).containsExactly(secondMember);
verify(memberService, times(1)).list(any(Criteria.class));
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
import net.ximatai.muyun.database.core.orm.PageRequest;
import net.ximatai.muyun.database.core.orm.PageResult;
import net.ximatai.muyun.database.core.orm.Sort;
import net.ximatai.muyun.spring.ability.reference.ReferenceReadFacade;
import net.ximatai.muyun.spring.iam.user.UserAccount;
import net.ximatai.muyun.spring.iam.user.UserAccountService;
import net.ximatai.muyun.spring.common.model.contract.EntityContract;
Expand All @@ -23,16 +22,13 @@ public class EmployeeEmploymentReadService {
private final EmployeeService employeeService;
private final EmployeeAccountService employeeAccountService;
private final UserAccountService userAccountService;
private final ReferenceReadFacade referenceReads;

public EmployeeEmploymentReadService(EmployeePositionService employeePositionService, EmployeeService employeeService,
EmployeeAccountService employeeAccountService,
UserAccountService userAccountService,
ReferenceReadFacade referenceReads) {
UserAccountService userAccountService) {
this.employeePositionService = employeePositionService; this.employeeService = employeeService;
this.employeeAccountService = employeeAccountService;
this.userAccountService = userAccountService;
this.referenceReads = referenceReads;
}

public PageResult<EmployeeEmploymentView> page(Query query) {
Expand Down Expand Up @@ -61,7 +57,6 @@ private PageResult<EmployeeEmploymentView> page(Query query, Map<String, Employe
if (normalized.organizationId() != null && !normalized.organizationId().isBlank()) criteria.eq("organizationId", normalized.organizationId().trim());
if (normalized.departmentId() != null && !normalized.departmentId().isBlank()) criteria.eq("departmentId", normalized.departmentId().trim());
PageResult<EmployeePosition> page = employeePositionService.pageQuery(criteria, normalized.pageRequest(), Sort.asc("employeeId"));
referenceReads.enrich(employeePositionService, page.getRecords());
return PageResult.of(views(page.getRecords(), employeeOverrides), page.getTotal(), normalized.pageRequest());
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import net.ximatai.muyun.database.core.orm.Criteria;
import net.ximatai.muyun.database.core.orm.PageRequest;
import net.ximatai.muyun.spring.common.identity.CurrentUser;
import net.ximatai.muyun.spring.ability.reference.ReferenceReadFacade;
import net.ximatai.muyun.spring.iam.employee.Employee;
import net.ximatai.muyun.spring.iam.employee.EmployeeAccountService;
import net.ximatai.muyun.spring.iam.employee.EmployeePosition;
Expand All @@ -20,16 +19,13 @@ public class CurrentUserProfileService {
private final EmployeeAccountService employeeAccountService;
private final EmployeeService employeeService;
private final EmployeePositionService employeePositionService;
private final ReferenceReadFacade referenceReads;

public CurrentUserProfileService(EmployeeAccountService employeeAccountService,
EmployeeService employeeService,
EmployeePositionService employeePositionService,
ReferenceReadFacade referenceReads) {
EmployeePositionService employeePositionService) {
this.employeeAccountService = employeeAccountService;
this.employeeService = employeeService;
this.employeePositionService = employeePositionService;
this.referenceReads = referenceReads;
}

public CurrentUserProfile currentProfile(CurrentUser currentUser) {
Expand Down Expand Up @@ -70,7 +66,6 @@ private CurrentUserProfile.EmployeeProfile employeeProfile(String userId) {
private List<CurrentUserProfile.PositionProfile> positions(String employeeId) {
List<EmployeePosition> relations = employeePositionService.list(
Criteria.of().eq("employeeId", employeeId).eq("enabled", true), new PageRequest(0, 100));
referenceReads.enrich(employeePositionService, relations);
return relations
.stream()
.map(relation -> new CurrentUserProfile.PositionProfile(relation.getPositionId(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,9 @@

import net.ximatai.muyun.database.core.orm.PageRequest;
import net.ximatai.muyun.database.core.orm.PageResult;
import net.ximatai.muyun.spring.ability.reference.ReferenceReadFacade;
import net.ximatai.muyun.spring.ability.reference.ReferenceLoadResolver;
import net.ximatai.muyun.spring.common.model.contract.EntityContract;
import net.ximatai.muyun.spring.iam.user.UserAccountService;
import org.junit.jupiter.api.Test;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

import static org.assertj.core.api.Assertions.assertThat;
Expand All @@ -33,30 +28,14 @@ void shouldKeepRetainedEmployeeFieldsInRecycleBinEmploymentProjection() {
employment.setId("employment-1");
employment.setEmployeeId("employee-1");
employment.setOrganizationId("org-1");
employment.setOrganizationTitle("机构一");
employment.setDepartmentId("dept-1");
employment.setDepartmentTitle("部门一");
employment.setPositionId("position-1");
employment.setPositionTitle("岗位一");
PageRequest page = PageRequest.of(1, 20);
List<List<? extends EntityContract>> enrichedBatches = new ArrayList<>();
ReferenceReadFacade referenceReads = new ReferenceReadFacade(new ReferenceLoadResolver() {
@Override
public void populate(net.ximatai.muyun.spring.ability.CrudAbility<?> ability, EntityContract entity) {
throw new AssertionError("employment read must use batch enrichment");
}

@Override
public void populateAll(net.ximatai.muyun.spring.ability.CrudAbility<?> ability,
Collection<? extends EntityContract> entities) {
enrichedBatches.add(List.copyOf(entities));
entities.forEach(entity -> {
EmployeePosition relation = (EmployeePosition) entity;
relation.setOrganizationTitle("机构一");
relation.setDepartmentTitle("部门一");
relation.setPositionTitle("岗位一");
});
}
});
EmployeeEmploymentReadService readService = new EmployeeEmploymentReadService(
employeePositionService, employeeService, employeeAccountService, userAccountService, referenceReads);
employeePositionService, employeeService, employeeAccountService, userAccountService);

when(employeePositionService.pageQuery(any(), any(), any()))
.thenReturn(PageResult.of(List.of(employment), 1, page));
Expand All @@ -74,6 +53,5 @@ public void populateAll(net.ximatai.muyun.spring.ability.CrudAbility<?> ability,
assertThat(view.departmentTitle()).isEqualTo("部门一");
assertThat(view.positionTitle()).isEqualTo("岗位一");
});
assertThat(enrichedBatches).containsExactly(List.of(employment));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@
import net.ximatai.muyun.database.core.orm.Criteria;
import net.ximatai.muyun.database.core.orm.PageRequest;
import net.ximatai.muyun.spring.common.identity.CurrentUser;
import net.ximatai.muyun.spring.ability.reference.ReferenceLoadResolver;
import net.ximatai.muyun.spring.ability.reference.ReferenceReadFacade;
import net.ximatai.muyun.spring.iam.employee.Employee;
import net.ximatai.muyun.spring.iam.employee.EmployeeAccountService;
import net.ximatai.muyun.spring.iam.employee.EmployeePositionService;
Expand All @@ -28,8 +26,7 @@ void shouldPersistOwnContactAndAvatarThroughTheSelfManagedProfileFacade() {
when(accountService.employeeIdOfUser("user-1")).thenReturn("employee-1");
when(employeeService.select("employee-1")).thenReturn(employee);
when(positionService.list(any(Criteria.class), any(PageRequest.class))).thenReturn(java.util.List.of());
CurrentUserProfileService service = new CurrentUserProfileService(accountService, employeeService, positionService,
new ReferenceReadFacade(ReferenceLoadResolver.NONE));
CurrentUserProfileService service = new CurrentUserProfileService(accountService, employeeService, positionService);

CurrentUserProfile profile = service.updateCurrentProfile(
CurrentUser.tenantUser("user-1", "demo.user", "tenant-a"),
Expand Down
Loading
Loading