PRIMARY KEY (book_id
, student_id
),
INDEX idx_appoint_time
(appoint_time
)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT=‘预约图书表’
在entity
包中添加两个对应的实体,图书实体Book.java
和预约图书实体Appointment.java
。
Book.java
package com.soecode.lyf.entity;
public class Book {
private long bookId;// 图书ID
private String name;// 图书名称
private int number;// 馆藏数量
// 省略构造方法,getter和setter方法,toString方法
}
Appointment.java
package com.soecode.lyf.entity;
import java.util.Date;
/**
* 预约图书实体
*/
public class Appointment {
private long bookId;// 图书ID
private long studentId;// 学号
private Date appointTime;// 预约时间
// 多对一的复合属性
private Book book;// 图书实体
// 省略构造方法,getter和setter方法,toString方法
}
在dao
包新建接口BookDao.java
和Appointment.java
BookDao.java
package com.soecode.lyf.dao;
import java.util.List;
import com.soecode.lyf.entity.Book;
public interface BookDao {
/**
* 通过ID查询单本图书
*
* @param id
* @return
*/
Book queryById(long id);
/**
* 查询所有图书
*
* @param offset 查询起始位置
* @param limit 查询条数
* @return
*/
List queryAll(@Param(“offset”) int offset, @Param(“limit”) int limit);
/**
* 减少馆藏数量
*
* @param bookId
* @return 如果影响行数等于>1,表示更新的记录行数
*/
int reduceNumber(long bookId);
}
AppointmentDao.java
package com.soecode.lyf.dao;
import org.apache.ibatis.annotations.Param;
import com.soecode.lyf.entity.Appointment;
public interface AppointmentDao {
/**
* 插入预约图书记录
*
* @param bookId
* @param studentId
* @return 插入的行数
*/
int insertAppointment(@Param(“bookId”) long bookId, @Param(“studentId”) long studentId);
/**
* 通过主键查询预约图书记录,并且携带图书实体
*
* @param bookId
* @param studentId
* @return
*/
Appointment queryByKeyWithBook(@Param(“bookId”) long bookId, @Param(“studentId”) long studentId);
}
提示:这里为什么要给方法的参数添加@Param
注解呢?是因为该方法有两个或以上的参数,一定要加,不然mybatis识别不了。上面的BookDao
接口的queryById
方法和reduceNumber
方法只有一个参数book_id
,所以可以不用加 @Param
注解,当然加了也无所谓~
注意,这里不需要实现dao接口不用编写daoImpl, mybatis会给我们动态实现,但是我们需要编写相应的mapper。
在mapper
目录里新建两个文件BookDao.xml
和AppointmentDao.xml
,分别对应上面两个dao接口,代码如下。
BookDao.xml
<?xml version="1.0" encoding="UTF-8"?> SELECT book_id, name, number FROM book WHERE book_id = #{bookId} SELECT book_id, name, number FROM book ORDER BY book_id LIMIT #{offset}, #{limit} UPDATE book SET number = number - 1 WHERE book_id = #{bookId} AND number > 0AppointmentDao.xml
<?xml version="1.0" encoding="UTF-8"?> INSERT ignore INTO appointment (book_id, student_id) VALUES (#{bookId}, #{studentId}) SELECT a.book_id, a.student_id, a.appoint_time, b.book_id "book.book_id", b.`name` "book.name", b.number "book.number" FROM appointment a INNER JOIN book b ON a.book_id = b.book_id WHERE a.book_id = #{bookId} AND a.student_id = #{studentId}mapper总结:namespace
是该xml对应的接口全名,select
和update
中的id
对应方法名,resultType
是返回值类型,parameterType
是参数类型(这个其实可选),最后#{...}
中填写的是方法的参数,看懂了是不是很简单!!我也这么觉得~ 还有一个小技巧要交给大家,就是在返回Appointment
对象包含了一个属性名为book
的Book对象,那么可以使用"book.属性名"
的方式来取值,看上面queryByKeyWithBook
方法的sql。
dao
层写完了,接下来test
对应的package
写我们测试方法吧。
因为我们之后会写很多测试方法,在测试前需要让程序读入spring-dao和mybatis等配置文件,所以我这里就抽离出来一个BaseTest
类,只要是测试方法就继承它,这样那些繁琐的重复的代码就不用写那么多了~
BaseTest.java
package com.soecode.lyf;
import org.junit.runner.RunWith;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
/**
* 配置spring和junit整合,junit启动时加载springIOC容器 spring-test,junit
*/
@RunWith(SpringJUnit4ClassRunner.class)
// 告诉junit spring配置文件
@ContextConfiguration({ “classpath:spring/spring-dao.xml”, “classpath:spring/spring-service.xml” })
public class BaseTest {
}
因为spring-service
在service
层的测试中会时候到,这里也一起引入算了!
新建BookDaoTest.java
和AppointmentDaoTest.java
两个dao测试文件。
BookDaoTest.java
package com.soecode.lyf.dao;
import java.util.List;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import com.soecode.lyf.BaseTest;
import com.soecode.lyf.entity.Book;
public class BookDaoTest extends BaseTest {
@Autowired
private BookDao bookDao;
@Test
public void testQueryById() throws Exception {
long bookId = 1000;
Book book = bookDao.queryById(bookId);
System.out.println(book);
}
@Test
public void testQueryAll() throws Exception {
List books = bookDao.queryAll(0, 4);
for (Book book : books) {
System.out.println(book);
}
}
@Test
public void testReduceNumber() throws Exception {
long bookId = 1000;
int update = bookDao.reduceNumber(bookId);
System.out.println(“update=” + update);
}
}
BookDaoTest测试结果
testQueryById
testQueryAll
testReduceNumber
AppointmentDaoTest.java
package com.soecode.lyf.dao;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import com.soecode.lyf.BaseTest;
import com.soecode.lyf.entity.Appointment;
public class AppointmentDaoTest extends BaseTest {
@Autowired
private AppointmentDao appointmentDao;
@Test
public void testInsertAppointment() throws Exception {
long bookId = 1000;
long studentId = 12345678910L;
int insert = appointmentDao.insertAppointment(bookId, studentId);
System.out.println(“insert=” + insert);
}
@Test
public void testQueryByKeyWithBook() throws Exception {
long bookId = 1000;
long studentId = 12345678910L;
Appointment appointment = appointmentDao.queryByKeyWithBook(bookId, studentId);
System.out.println(appointment);
System.out.println(appointment.getBook());
}
}
AppointmentDaoTest测试结果
testInsertAppointment
testQueryByKeyWithBook
嗯,到这里一切到很顺利那么我们继续service层的编码吧可能下面开始信息里比较大,大家要做好心理准备~
首先,在写我们的控制器之前,我们先定义几个预约图书操作返回码的数据字典,也就是我们要返回给客户端的信息。我们这类使用枚举类,没听过的小伙伴要好好恶补一下了(我也是最近才学到的= =)
预约业务操作返回码说明
返回码 | 说明 |
---|---|
1 | 预约成功 |
0 | 库存不足 |
-1 | 重复预约 |
-2 | 系统异常 |
新建一个包叫enums
,在里面新建一个枚举类AppointStateEnum.java
,用来定义预约业务的数据字典,没听懂没关系,我们直接看代码吧~是不是感觉有模有样了!
AppointStateEnum.java
package com.soecode.lyf.enums;
/**
* 使用枚举表述常量数据字典
*/
public enum AppointStateEnum {
SUCCESS(1, “预约成功”), NO_NUMBER(0, “库存不足”), REPEAT_APPOINT(-1, “重复预约”), INNER_ERROR(-2, “系统异常”);
private int state;
private String stateInfo;
private AppointStateEnum(int state, String stateInfo) {
this.state = state;
this.stateInfo = stateInfo;
}
public int getState() {
return state;
}
public String getStateInfo() {
return stateInfo;
}
public static AppointStateEnum stateOf(int index) {
for (AppointStateEnum state : values()) {
if (state.getState() == index) {
return state;
}
}
return null;
}
}
接下来,在dto
包下新建AppointExecution.java
用来存储我们执行预约操作的返回结果。
AppointExecution.java
package com.soecode.lyf.dto;
import com.soecode.lyf.entity.Appointment;
import com.soecode.lyf.enums.AppointStateEnum;
/**
* 封装预约执行后结果
*/
public class AppointExecution {
// 图书ID
private long bookId;
// 秒杀预约结果状态
private int state;
// 状态标识
private String stateInfo;
// 预约成功对象
private Appointment appointment;
public AppointExecution() {
}
// 预约失败的构造器
public AppointExecution(long bookId, AppointStateEnum stateEnum) {
this.bookId = bookId;
this.state = stateEnum.getState();
this.stateInfo = stateEnum.getStateInfo();
}
// 预约成功的构造器
public AppointExecution(long bookId, AppointStateEnum stateEnum, Appointment appointment) {
this.bookId = bookId;
this.state = stateEnum.getState();
this.stateInfo = stateEnum.getStateInfo();
this.appointment = appointment;
}
// 省略getter和setter方法,toString方法
}
接着,在exception
包下新建三个文件
NoNumberException.java
RepeatAppointException.java
AppointException.java
预约业务异常类(都需要继承RuntimeException),分别是无库存异常、重复预约异常、预约未知错误异常,用于业务层非成功情况下的返回(即成功返回结果,失败抛出异常)。
NoNumberException.java
package com.soecode.lyf.exception;
/**
* 库存不足异常
*/
public class NoNumberException extends RuntimeException {
public NoNumberException(String message) {
super(message);
}
public NoNumberException(String message, Throwable cause) {
super(message, cause);
}
}
RepeatAppointException.java
package com.soecode.lyf.exception;
/**
* 重复预约异常
*/
public class RepeatAppointException extends RuntimeException {
public RepeatAppointException(String message) {
super(message);
}
public RepeatAppointException(String message, Throwable cause) {
super(message, cause);
}
}
AppointException.java
package com.soecode.lyf.exception;
/**
* 预约业务异常
*/
public class AppointException extends RuntimeException {
public AppointException(String message) {
super(message);
}
public AppointException(String message, Throwable cause) {
super(message, cause);
}
}
咱们终于可以编写业务代码了,在service
包下新建BookService.java
图书业务接口。
BookService.java
package com.soecode.lyf.service;
import java.util.List;
import com.soecode.lyf.dto.AppointExecution;
import com.soecode.lyf.entity.Book;
/**
* 业务接口:站在"使用者"角度设计接口 三个方面:方法定义粒度,参数,返回类型(return 类型/异常)
*/
public interface BookService {
/**
* 查询一本图书
*
* @param bookId
* @return
*/
Book getById(long bookId);
/**
* 查询所有图书
*
* @return
*/
List getList();
/**
* 预约图书
*
* @param bookId
* @param studentId
* @return
*/
AppointExecution appoint(long bookId, long studentId);
}
在service.impl
包下新建BookServiceImpl.java
使用BookService
接口,并实现里面的方法。
BookServiceImpl
package com.soecode.lyf.service.impl;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.soecode.lyf.dao.AppointmentDao;
import com.soecode.lyf.dao.BookDao;
import com.soecode.lyf.dto.AppointExecution;
import com.soecode.lyf.entity.Appointment;
import com.soecode.lyf.entity.Book;
import com.soecode.lyf.enums.AppointStateEnum;
import com.soecode.lyf.exception.AppointException;
import com.soecode.lyf.exception.NoNumberException;
import com.soecode.lyf.exception.RepeatAppointException;
import com.soecode.lyf.service.BookService;
@Service
public class BookServiceImpl implements BookService {
private Logger logger = LoggerFactory.getLogger(this.getClass());
// 注入Service依赖
@Autowired
private BookDao bookDao;
@Autowired
private AppointmentDao appointmentDao;
@Override
public Book getById(long bookId) {
return bookDao.queryById(bookId);
}
@Override
public List getList() {
return bookDao.queryAll(0, 1000);
}
@Override
@Transactional
/**
* 使用注解控制事务方法的优点: 1.开发团队达成一致约定,明确标注事务方法的编程风格
* 2.保证事务方法的执行时间尽可能短,不要穿插其他网络操作,RPC/HTTP请求或者剥离到事务方法外部
* 3.不是所有的方法都需要事务,如只有一条修改操作,只读操作不需要事务控制
*/
public AppointExecution appoint(long bookId, long studentId) {
try {
// 减库存
int update = bookDao.reduceNumber(bookId);
if (update <= 0) {// 库存不足
//return new AppointExecution(bookId, AppointStateEnum.NO_NUMBER);//错误写法
throw new NoNumberException(“no number”);
} else {
// 执行预约操作
int insert = appointmentDao.insertAppointment(bookId, studentId);
if (insert <= 0) {// 重复预约
//return new AppointExecution(bookId, AppointStateEnum.REPEAT_APPOINT);//错误写法
throw new RepeatAppointException(“repeat appoint”);
} else {// 预约成功
Appointment appointment = appointmentDao.queryByKeyWithBook(bookId, studentId);
return new AppointExecution(bookId, AppointStateEnum.SUCCESS, appointment);
}
}
// 要先于catch Exception异常前先catch住再抛出,不然自定义的异常也会被转换为AppointException,导致控制层无法具体识别是哪个异常
} catch (NoNumberException e1) {
throw e1;
} catch (RepeatAppointException e2) {
throw e2;
} catch (Exception e) {
logger.error(e.getMessage(), e);
// 所有编译期异常转换为运行期异常
//return new AppointExecution(bookId, AppointStateEnum.INNER_ERROR);//错误写法
throw new AppointException(“appoint inner error:” + e.getMessage());
}
}
}
下面我们来测试一下我们的业务代码吧~因为查询图书的业务不复杂,所以这里只演示我们最重要的预约图书业务!!
BookServiceImplTest.java
package com.soecode.lyf.service.impl;
import static org.junit.Assert.fail;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import com.soecode.lyf.BaseTest;
import com.soecode.lyf.dto.AppointExecution;
import com.soecode.lyf.service.BookService;
public class BookServiceImplTest extends BaseTest {
@Autowired
private BookService bookService;
@Test
public void testAppoint() throws Exception {
long bookId = 1001;
long studentId = 12345678910L;
AppointExecution execution = bookService.appoint(bookId, studentId);
System.out.println(execution);
}
}
BookServiceImplTest测试结果
testAppoint
首次执行是“预约成功”,如果再次执行的话,应该会出现“重复预约”,哈哈,我们所有的后台代码都通过单元测试啦~~是不是很开心~
咱们还需要在dto
包里新建一个封装json返回结果的类Result.java
,设计成泛型。
Result.java
package com.soecode.lyf.dto;
/**
* 封装json对象,所有返回结果都使用它
*/
public class Result {
private boolean success;// 是否成功标志
private T data;// 成功时返回的数据
private String error;// 错误信息
public Result() {
}
// 成功时的构造器
public Result(boolean success, T data) {
this.success = success;
this.data = data;
}
// 错误时的构造器
public Result(boolean success, String error) {
this.success = success;
this.error = error;
}
// 省略getter和setter方法
}
最后,我们写web层,也就是controller,我们在web
包下新建BookController.java
文件。
BookController.java
package com.soecode.lyf.web;
import java.util.List;
import org.apache.ibatis.annotations.Param;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import com.soecode.lyf.dto.AppointExecution;
import com.soecode.lyf.dto.Result;
import com.soecode.lyf.entity.Book;
import com.soecode.lyf.enums.AppointStateEnum;
import com.soecode.lyf.exception.NoNumberException;
import com.soecode.lyf.exception.RepeatAppointException;
import com.soecode.lyf.service.BookService;
@Controller
@RequestMapping(“/book”) // url:/模块/资源/{id}/细分 /seckill/list
public class BookController {
private Logger logger = LoggerFactory.getLogger(this.getClass());
@Autowired
private BookService bookService;
@RequestMapping(value = “/list”, method = RequestMethod.GET)
private String list(Model model) {
List list = bookService.getList();
model.addAttribute(“list”, list);
// list.jsp + model = ModelAndView
return “list”;// WEB-INF/jsp/“list”.jsp
}
@RequestMapping(value = “/{bookId}/detail”, method = RequestMethod.GET)
private String detail(@PathVariable(“bookId”) Long bookId, Model model) {
if (bookId == null) {
return “redirect:/book/list”;
}
Book book = bookService.getById(bookId);
if (book == null) {
return “forward:/book/list”;
}
model.addAttribute(“book”, book);
return “detail”;
}
//ajax json
@RequestMapping(value = “/{bookId}/appoint”, method = RequestMethod.POST, produces = {
“application/json; charset=utf-8” })
@ResponseBody
private Result appoint(@PathVariable(“bookId”) Long bookId, @RequestParam(“studentId”) Long studentId) {
if (studentId == null || studentId.equals(“”)) {
return new Result<>(false, “学号不能为空”);
}
//AppointExecution execution = bookService.appoint(bookId, studentId);//错误写法,不能统一返回,要处理异常(失败)情况
AppointExecution execution = null;
try {
execution = bookService.appoint(bookId, studentId);
} catch (NoNumberException e1) {
execution = new AppointExecution(bookId, AppointStateEnum.NO_NUMBER);
} catch (RepeatAppointException e2) {
execution = new AppointExecution(bookId, AppointStateEnum.REPEAT_APPOINT);
} catch (Exception e) {
execution = new AppointExecution(bookId, AppointStateEnum.INNER_ERROR);
}
return new Result(true, execution);
}
}
因为我比较懒,所以我们就不测试controller了,好讨厌写前端,呜呜呜~
到此,我们的SSM框架整合配置,与应用实例部分已经结束了,我把所有源码和jar包一起打包放在了我的GitHub上,需要的可以去下载,喜欢就给个star吧,这篇东西写了两个晚上也不容易啊。
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数Go语言工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Go语言全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Golang知识点,真正体系化!
由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新
如果你觉得这些内容对你有帮助,可以添加V获取:vip1024b (备注Go)
一个人可以走的很快,但一群人才能走的更远。不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎扫码加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
ion>(true, execution);
}
}
因为我比较懒,所以我们就不测试controller了,好讨厌写前端,呜呜呜~
到此,我们的SSM框架整合配置,与应用实例部分已经结束了,我把所有源码和jar包一起打包放在了我的GitHub上,需要的可以去下载,喜欢就给个star吧,这篇东西写了两个晚上也不容易啊。
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数Go语言工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Go语言全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
[外链图片转存中…(img-OyPbSSyF-1713032571538)]
[外链图片转存中…(img-PdSLNCn4-1713032571538)]
[外链图片转存中…(img-i5Hi6Tp8-1713032571539)]
[外链图片转存中…(img-NypuoYQJ-1713032571540)]
[外链图片转存中…(img-4AJN03zg-1713032571540)]
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Golang知识点,真正体系化!
由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新
如果你觉得这些内容对你有帮助,可以添加V获取:vip1024b (备注Go)
[外链图片转存中…(img-bd7DX1Er-1713032571541)]
一个人可以走的很快,但一群人才能走的更远。不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎扫码加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!