ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Java图书管理系统CRUD实战与数据库设计

Java图书管理系统CRUD实战与数据库设计

1. 图书管理系统中的增删改查实战指南

每次看到新入行的开发者在面试中被"实现一个图书管理系统"这类题目难住时,我都想起自己早年用记事本写Java连接MySQL的囧事。增删改查(CRUD)就像编程界的"四则运算"——看似简单却暗藏玄机。以图书管理为例,一个完整的CRUD实现需要前后端配合、数据库设计、业务逻辑验证等多维度考量。下面我就用十年来踩过的坑,带你从零实现一套健壮的图书管理系统。

2. 数据库设计与建表

2.1 表结构设计要点

图书表(books)基础字段设计:

CREATE TABLE `books` ( `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键', `isbn` varchar(20) NOT NULL COMMENT '国际标准书号', `title` varchar(100) NOT NULL COMMENT '书名', `author` varchar(50) NOT NULL COMMENT '作者', `publisher` varchar(50) DEFAULT NULL COMMENT '出版社', `publish_date` date DEFAULT NULL COMMENT '出版日期', `price` decimal(10,2) DEFAULT NULL COMMENT '定价', `stock` int(11) NOT NULL DEFAULT '0' COMMENT '库存量', `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', PRIMARY KEY (`id`), UNIQUE KEY `idx_isbn` (`isbn`), KEY `idx_author` (`author`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='图书信息表';

关键设计原则:

  1. 使用自增主键提高插入性能
  2. ISBN设置唯一索引避免重复录入
  3. 作者字段添加普通索引优化查询
  4. 价格字段使用decimal避免浮点精度问题
  5. 自动维护创建/更新时间

2.2 关联表设计

图书分类关联表(book_category)示例:

CREATE TABLE `book_category` ( `id` int(11) NOT NULL AUTO_INCREMENT, `book_id` int(11) NOT NULL COMMENT '图书ID', `category_id` int(11) NOT NULL COMMENT '分类ID', PRIMARY KEY (`id`), UNIQUE KEY `uk_book_category` (`book_id`,`category_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

3. 增删改查核心实现

3.1 新增图书(Create)

Java+MyBatis实现示例:

// 使用@Valid注解进行参数校验 @PostMapping("/books") public Result addBook(@Valid @RequestBody Book book) { // ISBN唯一性校验 if(bookService.checkIsbnExist(book.getIsbn())){ return Result.error("ISBN已存在"); } // 设置默认库存 if(book.getStock() == null){ book.setStock(0); } // 事务处理 return transactionTemplate.execute(status -> { try { bookMapper.insert(book); // 处理分类关联 if(!CollectionUtils.isEmpty(book.getCategoryIds())){ bookCategoryMapper.batchInsert(book.getId(), book.getCategoryIds()); } return Result.success(book.getId()); } catch (Exception e) { status.setRollbackOnly(); log.error("添加图书失败", e); return Result.error("系统异常"); } }); }

常见问题处理:

  1. 批量插入性能优化:使用MyBatis的<foreach>标签
  2. 事务边界控制:非必要字段不要放在事务内
  3. 唯一索引冲突:先查询再插入存在并发问题,推荐使用INSERT IGNORE

3.2 查询图书(Retrieve)

3.2.1 基础查询
@GetMapping("/books/{id}") public Result getBook(@PathVariable Integer id) { Book book = bookMapper.selectById(id); if(book == null){ return Result.error("图书不存在"); } // 查询关联分类 List<Integer> categoryIds = bookCategoryMapper.selectCategoryIdsByBookId(id); book.setCategoryIds(categoryIds); return Result.success(book); }
3.2.2 分页查询
@GetMapping("/books") public Result listBooks( @RequestParam(required = false) String title, @RequestParam(required = false) String author, @RequestParam(defaultValue = "1") Integer pageNum, @RequestParam(defaultValue = "10") Integer pageSize) { PageHelper.startPage(pageNum, pageSize); List<Book> books = bookMapper.selectByCondition(title, author); PageInfo<Book> pageInfo = new PageInfo<>(books); return Result.success(pageInfo); }

性能优化技巧:

  • 避免使用SELECT *,只查询必要字段
  • 大数据量表使用延迟关联优化分页
  • 模糊查询LIKE '%关键词%'会导致索引失效

3.3 更新图书(Update)

@PutMapping("/books/{id}") public Result updateBook(@PathVariable Integer id, @Valid @RequestBody Book book) { Book existBook = bookMapper.selectById(id); if(existBook == null){ return Result.error("图书不存在"); } // ISBN不可修改 if(!existBook.getIsbn().equals(book.getIsbn())){ return Result.error("ISBN不允许修改"); } book.setId(id); bookMapper.updateById(book); // 更新分类关联(先删后增) bookCategoryMapper.deleteByBookId(id); if(!CollectionUtils.isEmpty(book.getCategoryIds())){ bookCategoryMapper.batchInsert(id, book.getCategoryIds()); } return Result.success(); }

更新操作的注意事项:

  1. 重要字段(如ISBN)应禁止修改
  2. 关联数据更新需要保持原子性
  3. 记录修改日志用于审计追踪

3.4 删除图书(Delete)

@DeleteMapping("/books/{id}") public Result deleteBook(@PathVariable Integer id) { // 存在性校验 if(bookMapper.selectById(id) == null){ return Result.error("图书不存在"); } // 检查是否有借阅记录 if(borrowRecordMapper.countByBookId(id) > 0){ return Result.error("存在借阅记录不可删除"); } transactionTemplate.execute(status -> { bookMapper.deleteById(id); bookCategoryMapper.deleteByBookId(id); return null; }); return Result.success(); }

删除策略选择:

  1. 物理删除:直接DELETE(需处理关联数据)
  2. 逻辑删除:使用is_deleted字段标记
  3. 归档删除:移动到历史表

4. 高级功能实现

4.1 批量导入导出

使用Apache POI实现Excel导入:

public Result importBooks(MultipartFile file) { try (InputStream is = file.getInputStream()) { Workbook workbook = new XSSFWorkbook(is); Sheet sheet = workbook.getSheetAt(0); List<Book> books = new ArrayList<>(); for (Row row : sheet) { if(row.getRowNum() == 0) continue; // 跳过标题行 Book book = new Book(); book.setIsbn(row.getCell(0).getStringCellValue()); book.setTitle(row.getCell(1).getStringCellValue()); // 其他字段处理... books.add(book); } // 批量插入 bookMapper.batchInsert(books); return Result.success(books.size()); } catch (Exception e) { log.error("导入失败", e); return Result.error("文件解析错误"); } }

4.2 缓存优化方案

Redis缓存实现示例:

@Cacheable(value = "books", key = "#id") public Book getBookWithCache(Integer id) { return bookMapper.selectById(id); } @CachePut(value = "books", key = "#book.id") public Book updateBookWithCache(Book book) { bookMapper.updateById(book); return book; } @CacheEvict(value = "books", key = "#id") public void deleteBookWithCache(Integer id) { bookMapper.deleteById(id); }

缓存策略建议:

  1. 读多写少的数据适合缓存
  2. 设置合理的过期时间(如30分钟)
  3. 高并发场景考虑使用互斥锁防止缓存击穿

5. 安全与性能考量

5.1 SQL注入防护

错误示范:

@GetMapping("/books/unsafe") public List<Book> unsafeQuery(@RequestParam String title) { String sql = "SELECT * FROM books WHERE title LIKE '%" + title + "%'"; return jdbcTemplate.query(sql, new BeanPropertyRowMapper<>(Book.class)); }

正确做法:

@GetMapping("/books/safe") public List<Book> safeQuery(@RequestParam String title) { return bookMapper.selectByTitle("%" + title + "%"); }

5.2 接口幂等性设计

POST请求幂等处理:

@PostMapping("/books/idempotent") public Result addBookIdempotent(@RequestHeader("X-Request-Id") String requestId, @Valid @RequestBody Book book) { // 检查请求ID是否已处理 if(redisTemplate.opsForValue().setIfAbsent("req:"+requestId, "1", 24, TimeUnit.HOURS)){ return bookService.addBook(book); } return Result.error("请勿重复提交"); }

6. 前端交互建议

6.1 Vue.js实现示例

图书列表组件:

<template> <div> <el-table :data="books" border> <el-table-column prop="title" label="书名"></el-table-column> <el-table-column prop="author" label="作者"></el-table-column> <el-table-column label="操作"> <template #default="{row}"> <el-button @click="handleEdit(row)">编辑</el-button> <el-button type="danger" @click="handleDelete(row.id)">删除</el-button> </template> </el-table-column> </el-table> <el-pagination @current-change="handlePageChange" :current-page="pagination.page" :page-size="pagination.size" :total="pagination.total"> </el-pagination> </div> </template> <script> export default { data() { return { books: [], pagination: { page: 1, size: 10, total: 0 } } }, mounted() { this.fetchBooks(); }, methods: { async fetchBooks() { const res = await this.$http.get('/books', { params: { pageNum: this.pagination.page, pageSize: this.pagination.size } }); this.books = res.data.list; this.pagination.total = res.data.total; }, handlePageChange(page) { this.pagination.page = page; this.fetchBooks(); } } } </script>

6.2 表单验证策略

Element UI表单验证示例:

<el-form :model="book" :rules="rules" ref="form"> <el-form-item label="ISBN" prop="isbn"> <el-input v-model="book.isbn"></el-input> </el-form-item> <el-form-item label="书名" prop="title"> <el-input v-model="book.title"></el-input> </el-form-item> <!-- 其他字段 --> </el-form> <script> export default { data() { return { book: { isbn: '', title: '', // 其他字段 }, rules: { isbn: [ { required: true, message: '请输入ISBN', trigger: 'blur' }, { pattern: /^\d{13}$/, message: 'ISBN必须为13位数字' } ], title: [ { required: true, message: '请输入书名', trigger: 'blur' }, { min: 1, max: 100, message: '长度在1到100个字符' } ] } } } } </script>

7. 测试方案设计

7.1 单元测试用例

JUnit测试示例:

@SpringBootTest public class BookServiceTest { @Autowired private BookService bookService; @Test public void testAddBook() { Book book = new Book(); book.setIsbn("9787115470662"); book.setTitle("测试驱动开发"); book.setAuthor("Kent Beck"); Integer id = bookService.addBook(book); assertNotNull(id); Book savedBook = bookService.getBook(id); assertEquals("测试驱动开发", savedBook.getTitle()); } @Test public void testAddDuplicateIsbn() { Book book1 = new Book(); book1.setIsbn("9787121377963"); book1.setTitle("代码整洁之道"); bookService.addBook(book1); Book book2 = new Book(); book2.setIsbn("9787121377963"); book2.setTitle("重复ISBN测试"); assertThrows(BusinessException.class, () -> { bookService.addBook(book2); }); } }

7.2 性能测试要点

使用JMeter测试建议:

  1. 并发查询测试:模拟100-1000并发查询图书列表
  2. 混合场景测试:读写比例按8:2配置
  3. 长时间稳定性测试:持续运行12小时以上
  4. 重点关注指标:
    • 平均响应时间(<500ms)
    • 错误率(<0.1%)
    • 吞吐量(每秒请求数)

8. 部署与监控

8.1 数据库连接池配置

Druid推荐配置:

spring: datasource: type: com.alibaba.druid.pool.DruidDataSource druid: url: jdbc:mysql://localhost:3306/library?useSSL=false username: root password: 123456 initial-size: 5 min-idle: 5 max-active: 20 max-wait: 60000 time-between-eviction-runs-millis: 60000 min-evictable-idle-time-millis: 300000 validation-query: SELECT 1 test-while-idle: true test-on-borrow: false test-on-return: false

8.2 Prometheus监控指标

自定义业务指标示例:

@RestController public class BookController { private final Counter addBookCounter = Counter.build() .name("library_books_add_total") .help("Total number of books added") .register(); @PostMapping("/books") public Result addBook(@RequestBody Book book) { // 业务逻辑... addBookCounter.inc(); return Result.success(); } }

监控看板应包含:

  1. 接口QPS/耗时
  2. 数据库连接池状态
  3. 缓存命中率
  4. JVM内存/GC情况

9. 项目演进方向

9.1 微服务化改造

建议拆分方向:

  1. 图书服务:核心CRUD功能
  2. 库存服务:独立管理库存逻辑
  3. 搜索服务:Elasticsearch实现全文检索
  4. 推荐服务:基于用户行为的智能推荐

9.2 弹性设计考虑

应对高并发的策略:

  1. 数据库读写分离
  2. 热点数据多级缓存
  3. 请求限流与降级
  4. 异步处理非核心流程

十年经验告诉我,CRUD看似简单,但魔鬼藏在细节里。一个图书ISBN校验的疏忽可能导致数据混乱,一次不加事务的更新可能引发关联数据不一致。建议在开发过程中建立完整的校验机制和监控体系,把简单的增删改查做出工业级可靠性。

返回列表