1. 项目背景与核心需求
在后端开发中,基于分类ID查询菜品是一个典型的业务场景。我最近在开发一个餐饮管理系统时,就遇到了这个需求。系统需要根据前端传递的菜品分类ID,快速返回该分类下的所有菜品信息。
这种查询在电商、点餐系统、内容管理等场景中非常常见。比如外卖APP中点击"川菜"分类,就要展示所有川菜菜品;内容管理系统里选择"科技"分类,就要列出所有科技类文章。
2. 技术方案设计
2.1 数据库设计
首先需要设计合理的数据库表结构。我采用了最常见的两表设计:
-
分类表(category):
- id: 主键
- name: 分类名称
- description: 分类描述
-
菜品表(dish):
- id: 主键
- name: 菜品名称
- price: 价格
- category_id: 外键,关联分类表
- 其他菜品属性...
这种设计遵循了数据库范式,通过外键建立了明确的关联关系。
2.2 Spring Boot实现方案
在Spring Boot中,我选择了以下技术栈:
- Spring Data JPA:简化数据库操作
- Hibernate:作为JPA的实现
- RESTful API:提供查询接口
主要实现步骤:
- 创建实体类
- 定义Repository接口
- 实现Service层
- 创建Controller暴露API
3. 核心代码实现
3.1 实体类定义
java复制// 分类实体
@Entity
@Table(name = "category")
public class Category {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String description;
// getters and setters
}
// 菜品实体
@Entity
@Table(name = "dish")
public class Dish {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private BigDecimal price;
@ManyToOne
@JoinColumn(name = "category_id")
private Category category;
// getters and setters
}
3.2 Repository接口
java复制public interface DishRepository extends JpaRepository<Dish, Long> {
List<Dish> findByCategoryId(Long categoryId);
// 使用JPQL自定义查询
@Query("SELECT d FROM Dish d WHERE d.category.id = :categoryId")
List<Dish> findDishesByCategory(@Param("categoryId") Long categoryId);
}
3.3 Service层实现
java复制@Service
public class DishService {
@Autowired
private DishRepository dishRepository;
public List<Dish> getDishesByCategory(Long categoryId) {
return dishRepository.findByCategoryId(categoryId);
}
}
3.4 Controller层
java复制@RestController
@RequestMapping("/api/dishes")
public class DishController {
@Autowired
private DishService dishService;
@GetMapping("/by-category/{categoryId}")
public ResponseEntity<List<Dish>> getDishesByCategory(
@PathVariable Long categoryId) {
List<Dish> dishes = dishService.getDishesByCategory(categoryId);
return ResponseEntity.ok(dishes);
}
}
4. 性能优化与注意事项
4.1 N+1查询问题
使用JPA时容易遇到N+1查询问题。比如先查询分类,再循环查询每个分类下的菜品,会导致大量SQL执行。
解决方案:
- 使用@NamedEntityGraph定义查询图
- 使用JOIN FETCH
- 使用@EntityGraph注解
示例:
java复制@EntityGraph(attributePaths = {"category"})
List<Dish> findByCategoryId(Long categoryId);
4.2 分页查询
当菜品数量很多时,应该实现分页查询:
java复制Page<Dish> findByCategoryId(Long categoryId, Pageable pageable);
前端调用时传递page和size参数:
code复制/api/dishes/by-category/1?page=0&size=10
4.3 缓存策略
对于不常变动的菜品数据,可以考虑添加缓存:
- 使用Spring Cache注解:
java复制@Cacheable(value = "dishes", key = "#categoryId")
public List<Dish> getDishesByCategory(Long categoryId) {
// ...
}
- 配置Redis作为缓存实现
4.4 参数校验
必须对传入的categoryId进行校验:
java复制@GetMapping("/by-category/{categoryId}")
public ResponseEntity<List<Dish>> getDishesByCategory(
@PathVariable @Min(1) Long categoryId) {
// ...
}
5. 测试方案
5.1 单元测试
java复制@SpringBootTest
public class DishServiceTest {
@Autowired
private DishService dishService;
@Test
public void testGetDishesByCategory() {
List<Dish> dishes = dishService.getDishesByCategory(1L);
assertNotNull(dishes);
assertFalse(dishes.isEmpty());
}
}
5.2 集成测试
java复制@AutoConfigureMockMvc
@SpringBootTest
public class DishControllerTest {
@Autowired
private MockMvc mockMvc;
@Test
public void testGetDishesByCategory() throws Exception {
mockMvc.perform(get("/api/dishes/by-category/1"))
.andExpect(status().isOk())
.andExpect(jsonPath("$", hasSize(greaterThan(0))));
}
}
5.3 性能测试
使用JMeter测试接口响应时间,确保在并发情况下性能达标。
6. 常见问题与解决方案
6.1 分类不存在时的处理
当传入不存在的categoryId时,应该返回空列表还是404?
建议方案:
java复制public ResponseEntity<List<Dish>> getDishesByCategory(
@PathVariable Long categoryId) {
if (!categoryService.existsById(categoryId)) {
return ResponseEntity.notFound().build();
}
List<Dish> dishes = dishService.getDishesByCategory(categoryId);
return ResponseEntity.ok(dishes);
}
6.2 循环引用问题
当返回的JSON中包含双向引用时(Dish引用Category,Category又引用Dish),会导致序列化问题。
解决方案:
- 使用@JsonIgnore
- 使用DTO代替实体
- 使用@JsonIdentityInfo
6.3 多级分类查询
如果需要查询多级分类下的所有菜品(如查询"川菜"分类及其所有子分类下的菜品),实现方案:
- 分类表添加parent_id字段
- 使用递归CTE查询所有子分类ID
- 使用IN查询这些分类下的菜品
7. 扩展思考
7.1 关联查询的其他实现方式
除了JPA,还可以考虑:
- MyBatis:更灵活的SQL控制
- JDBC Template:更轻量级的方案
- Spring Data JDBC:介于JPA和JDBC之间
7.2 GraphQL方案
对于复杂的前端数据需求,可以考虑使用GraphQL:
graphql复制type Query {
dishesByCategory(categoryId: ID!): [Dish]
}
type Dish {
id: ID!
name: String!
price: Float!
category: Category!
}
7.3 微服务架构下的实现
在微服务架构中,菜品和分类可能属于不同服务。这时需要考虑:
- 服务间调用(Feign/RestTemplate)
- 数据一致性(分布式事务/最终一致性)
- API网关聚合数据
8. 项目总结
在实际开发中,分类查询虽然看似简单,但需要考虑很多细节问题。我在项目中遇到的几个关键点:
- 性能问题:最初没有注意N+1查询,导致接口响应慢
- 缓存策略:选择合适的缓存粒度很重要
- 异常处理:对非法categoryId要有合适的响应
最终的实现方案经过多次迭代,目前在生产环境运行稳定,平均响应时间在50ms以内。