Creating a dish with associated flavor options requires coordinated writes across two database tables: dish and dish_flavor. To preserve data integrity, these operations must execute as a single atomic unit—either all succeed or none do. This is achieved using Spring’s declarative transaction management.
The service layer method responsible for persisting both the dish and its flavors is annotated with @Transactional, ensuring rollback on any exception during execution. Transaction support is enabled global via @EnableTransactionManagement on the main application configuration class.
Since the DTO (DishDTO) contains both dish metadata and a dynamic list of flavors, the service method accepts the DTO but maps only the core dish fields to a Dish entity before insertion. After the dish record is saved, its auto-ganerated primary key is retrieved and assigned to each flavor entry. Only then are flavor records inserted in bulk.
The MyBatis mapper for Dish uses useGeneratedKeys="true" and keyProperty="id" in its XML <insert> definition to capture the newly generated ID. The flavor batch insert leverages MyBatis’ <foreach> tag to construct a multi-row INSERT statement efficiently.
Controller Layer
@RestController
@RequestMapping("/admin/dish")
@Api(tags = "Dish Management API")
@Slf4j
public class DishController {
@Autowired
private DishService dishService;
@PostMapping
@ApiOperation("Create a new dish with optional flavor variants")
public Result<Void> addDish(@RequestBody DishDTO payload) {
log.info("Received dish creation request: {}", payload);
dishService.persistDishAndFlavors(payload);
return Result.success();
}
}
Service Interface
public interface DishService {
/**
* Saves a dish and its associated flavor configurations atomically.
* @param payload Contains dish details and flavor list
*/
void persistDishAndFlavors(DishDTO payload);
}
Service Implementation
@Service
@Slf4j
@Transactional
public class DishServiceImpl implements DishService {
@Autowired
private DishMapper dishMapper;
@Autowired
private DishFlavorMapper flavorMapper;
@Override
public void persistDishAndFlavors(DishDTO payload) {
Dish dish = new Dish();
BeanUtils.copyProperties(payload, dish);
dishMapper.insert(dish); // Insert dish and retrieve generated ID
Long dishId = dish.getId();
List<DishFlavor> flavorList = payload.getFlavors();
if (CollectionUtils.isNotEmpty(flavorList)) {
flavorList.forEach(flavor -> flavor.setDishId(dishId));
flavorMapper.batchInsert(flavorList);
}
}
}
Dish Mapper Interface
@Mapper
public interface DishMapper {
@Select("SELECT COUNT(id) FROM dish WHERE category_id = #{categoryId}")
Integer countByCategory(Long categoryId);
@AutoFill(OperationType.INSERT)
void insert(Dish dish);
}
Dish Flavor Mapper Interface
@Mapper
public interface DishFlavorMapper {
void batchInsert(List<DishFlavor> flavors);
}
Dish Mapper XML Configuration
<?xml version="1.0" encoding="UTF-8"?>
<mapper namespace="com.sky.mapper.DishMapper">
<insert id="insert" useGeneratedKeys="true" keyProperty="id">
INSERT INTO dish (
name, category_id, price, image, description,
create_time, update_time, create_user, update_user, status
) VALUES (
#{name}, #{categoryId}, #{price}, #{image}, #{description},
#{createTime}, #{updateTime}, #{createUser}, #{updateUser}, #{status}
)
</insert>
</mapper>
Dish Flavor Mapper XML Configuration
<?xml version="1.0" encoding="UTF-8"?>
<mapper namespace="com.sky.mapper.DishFlavorMapper">
<insert id="batchInsert">
INSERT INTO dish_flavor (dish_id, name, value)
VALUES
<foreach collection="flavors" item="flavor" separator=",">
(#{flavor.dishId}, #{flavor.name}, #{flavor.value})
</foreach>
</insert>
</mapper>