One @Trans Annotation Solves N+1 Queries and Data Translation in Spring Boot
This article introduces Easy-Trans, a Dromara open-source library that uses a single @Trans annotation to automatically handle five types of data translation—dictionary, simple join, cross-service RPC, enum, and custom—while eliminating N+1 queries through batch ID collection and single IN-clause or cache lookups, with configuration details and best practices for Spring Boot applications.
Problem: Manual Translation Causes N+1 Queries
Business interfaces often need to translate database codes (user_id, status, school_id) into display names (user name, status text, school name). The naive approach iterates over each record and queries individually, producing a classic N+1 problem. For 100 orders, this means 100 separate selectById calls, plus additional queries for dictionaries, enums, and cross-service lookups, making code verbose and scattered.
for (OrderVO vo : voList) {
User user = userMapper.selectById(vo.getCreateUserId());
vo.setCreateUserName(user.getNickName());
}The resulting SQL log shows repeated single-row queries:
SELECT * FROM user WHERE id = 1;
SELECT * FROM user WHERE id = 2;
SELECT * FROM user WHERE id = 3;Solution: Easy-Trans with @Trans Annotation
Easy-Trans (Dromara) solves both translation and N+1 with one annotation. The VO implements TransPojo and fields are annotated with @Trans declaring translation type, data source, and target field. The example below covers all four common types:
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class StudentVO implements TransPojo {
// Dictionary translation: sex code -> sexName
@Trans(type = TransType.DICTIONARY, key = "sex", ref = "sexName")
private Integer sex;
private String sexName; // 男 / 女
// Simple join: schoolId -> schoolName via School table
@Trans(type = TransType.SIMPLE, target = School.class, fields = "schoolName")
private String schoolId;
// Cross-service RPC: call school-service batch proxy
@Trans(type = TransType.RPC,
targetClassName = "com.demo.pojo.School",
fields = "schoolName",
serviceName = "school-service",
alias = "middle")
private String middleSchoolId;
// Enum translation: ARTS -> 文科
@Trans(type = TransType.ENUM, key = "desc")
private StudentType studentType = StudentType.ARTS;
public enum StudentType {
ARTS("文科"),
SCIENCES("理科");
private final String desc;
StudentType(String desc) { this.desc = desc; }
}
}Five Translation Types and Data Sources
DICTIONARY : Dictionary cache (memory/Redis) — e.g., gender, status codes to text.
SIMPLE : Local database via ORM batch query — e.g., userId to userName.
RPC : Remote microservice via batch proxy endpoint — e.g., order service calling user service.
ENUM : Enum class attribute — e.g., enum constant to Chinese description.
AUTO : Custom method — special data sources or external APIs.
Dependencies and Configuration
Two dependencies required (version 2.3.1 for Spring Boot 3.x; 2.x starter not compatible with 3.x):
<dependency>
<groupId>org.dromara</groupId>
<artifactId>easy-trans-spring-boot-starter</artifactId>
<version>2.3.1</version>
</dependency>
<dependency>
<groupId>org.dromara</groupId>
<artifactId>easy-trans-mybatis-plus-extend</artifactId>
<version>2.3.1</version>
</dependency>YAML configuration:
easy-trans:
is-enable-redis: true # Enable Redis cache
is-enable-global: true # Global translation: intercept all ResponseBody
is-enable-tile: true # Tile mode: flatten result into separate fields
dict-use-redis: true # Store dictionary cache in RedisHow N+1 Is Eliminated: Three-Step Batch Process
The framework processes a collection in three steps: collect and deduplicate all IDs from @Trans fields, execute a single batch query (IN clause for SIMPLE, cache lookup for DICTIONARY, batch RPC for cross-service), then refill from the in-memory map. Query count becomes 1+1 regardless of list size.
public void translateBatch(List<OrderVO> list) {
// 1. Collect all non-null userIds and deduplicate
Set<Long> userIds = list.stream()
.map(OrderVO::getUserId)
.filter(Objects::nonNull)
.collect(Collectors.toSet());
if (userIds.isEmpty()) return;
// 2. Single IN query
List<User> users = userMapper.selectBatchIds(userIds);
Map<Long, String> idToName = users.stream()
.collect(Collectors.toMap(User::getId, User::getName));
// 3. Refill from memory map, zero SQL
for (OrderVO vo : list) {
vo.setUserName(idToName.get(vo.getUserId()));
}
}Dictionary translation is even more extreme: data loads into memory cache at startup/refresh, so translation never hits the database. A single field containing comma-separated IDs (e.g., "1,2,3") is split and merged into the same batch query.
Automatic vs Manual Triggering
Global auto-translation : With is-enable-global: true, the framework wraps the message converter, scans the response body before writing, and translates any @Trans fields automatically. Controller code stays clean:
@GetMapping("/list")
public List<StudentVO> list() {
return studentService.list(); // Returns, auto-translated
}Manual triggering : Explicitly call transService.transBatch(list) for performance-critical endpoints to avoid reflection overhead.
@Autowired
private TransService transService;
public List<StudentVO> list() {
List<StudentVO> list = studentService.list();
transService.transBatch(list);
return list;
}Internal Mechanism: Reflection, Grouping, and TransRepository
For a batch of objects, the framework: (1) reflects each object's @Trans fields, groups by translation type, collects keys; (2) delegates each group to its TransRepository for batch query — SIMPLE uses IN, DICTIONARY uses cache, RPC uses batch proxy; (3) writes results back to the ref fields. The MyBatis-Plus extension selects only primary key and required fields, not full rows. Swapping ORM (JPA, BeetlSQL) only requires replacing the extension package; annotations and business code remain unchanged.
An optional second-level cache stores batch query results in Redis keyed by ID. Subsequent requests skip the IN query entirely; consistency is maintained via TTL or active refresh on data change.
Cross-Service Translation: One Batch RPC Call
Instead of N Feign calls, the framework collects all IDs from the local batch, calls the target service's unified proxy endpoint /easyTrans/proxy once with the ID list (e.g., [1,2,...,100]). The remote service internally runs a single IN query and returns a map (e.g., {1:"张三", 2:"李四", ...}). The local service refills the fields. This saves 99 network round-trips, 99 serializations, and 99 database connections. Extra setup: gateway whitelist for /easyTrans/proxy and a custom RestTemplate with service-to-service authentication.
Enum and Reverse Translation
Enum translation is pure in-memory: map enum constant to a description field via key. Reverse translation (name to ID/code) is supported for search/filter scenarios using easy-trans-untrans-driver.
Critical Details and Best Practices
IN clause limits : Very large pages (tens of thousands) cause long IN lists that slow MySQL. Use pagination (keep page size small) or split IDs into batches of ~1000, merge results.
Cache consistency : Dictionary/association caches can serve stale data. Strategy: active refresh on change + TTL fallback; for association caches, set short TTL or evict keys on data change.
Global translation overhead : Reflection scans every ResponseBody. For high-performance endpoints, disable is-enable-global and use manual transBatch. Avoid implementing TransPojo on VOs that don't need translation.
Tile mode requirements : Target fields must exist and have matching types; code and text fields must be paired; numeric dictionary codes must use consistent Integer/Long.
RPC gateway : /easyTrans/proxy is often blocked by login interceptors; whitelist it and configure service-to-service token propagation, otherwise translation fails silently.
Author's personal checklist: always batch-translate VO collections; prefer dictionary/enum (pure memory, most stable); SIMPLE joins select only needed fields plus cache; microservices use RPC batch, never N Feign calls; paginate large collections and batch IN clauses; for extreme performance, disable global translation and call transBatch manually.
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.
Java Tech Workshop
Focused on Java backend technologies, sharing fundamentals, multithreading, JVM, the Spring ecosystem, microservices, distributed systems, high concurrency, source‑code analysis, and practical experience. Continuously delivers high‑quality original content, interview guides, and learning roadmaps to help Java developers progress from beginner to advanced, enhancing technical skills and core competitiveness.
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.
