java基础、数据库(mysql+jdbc),javaEE,SSM框架,redis,nigix,idea,maven,git,springboot
母婴、中小学生、职业教育都对在线教育有需求。
常见的商业模式有:
B2C:即商家到用户,主要有两个角色:管理员和普通用户,二者权限分别为:管理员:增删改 普通用户:查。核心模块是课程模块。
B2B2C:即商家到商家再到客户。比如京东:普通用户,可以买自营,可以买普通商家。
分为系统后台(管理员使用)和系统前台(普通用户使用)。
系统后台:
1)讲师管理模块
2)课程分类管理模块
3)课程管理模块
4)统计分析模块
5)订单管理模块
6)banner管理
7)权限管理
系统前台:
1)首页数据显示
2)讲师列表及详情
3)课程列表及在线播放
4)登录即注册
5)微信扫码登录
6)微信扫描支付
项目采用前后端分离开发,使用技术如下。
后端:springboot、springcloud、mybatis-plus,spring security,redis.maven,easyExcel,jwt,OAuth2
前端:Vue+element-ui+axios+node.js
其他:oss,视频点播服务,短信服务,微信支付和登录,docker,jenkins
Mybatis-plus对Mybatis只做增强不做改变,能简化开发提高效率。
1)下载mysql或者其它数据库。具体的安装过程可以参考博客https://www.php.cn/mysql-tutorials-454993.html
2)使用图形化界面workbench(navicat)创建数据库
在安装mysql后,我们通过cmd登录masql
mysql -u root -p
cmd报错:ERROR 2003 (HY000): Can’t connect to MySQL server on ‘localhost:3306’ (10061)
这是因为mysql的服务没有起来,在系统服务界面启动mysql。
重新尝试连接数据库,发现报错:ERROR 1045 (28000): Access denied for user ‘root’@‘localhost’ (using password: YES)
这是因为初始化密码输入错误。我们已经忘记了初始化密码,可以在mysql安装目录找回。找到mysql安装目录data文件夹下面的XXX.err文件,查找密码,这里我们发现密码没有被记录在这个文件中。
参考博客mysql安装时没记住初始密码报错ERROR 1045 (28000) (我的MySQL版本是8.0.18-winx64)_senlinyang_hong的博客-CSDN博客将mysql卸载重新安装。
在输入net start mysql命令启动服务器时报错:发生系统错误 2。系统找不到指定的文件。
参考博客mysql 启动报错–发现系统错误2,系统找不到指定的文件。_Marvel__Dead 胡艺宝的博客-CSDN博客解决,注意mysqld在bin目录下。
但是使用初始密码依旧无法登录mysql,参考博客已无密码形式登录再修改密码:ERROR 1045 (28000): Access denied for user ‘root‘@‘localhost‘ (using password: NO)解决办法_一个超会写Bug的程序猿的博客-CSDN博客,终于修改成功,这里重置的密码最好同时包含大小写字母以及数字(有说法表示MySQL要求密码符合以上规则,否则无法重置成功)。同时,在最后登录验证的时候把其他窗口都关闭重开一个,否则可能还是通过无密码模式在操作。
由于使用纯粹的命令行操作不方便,这里我们下载Navicat图形化界面操作Mysql数据库。使用时出现以下问题:
参考博客navicat连接异常,错误编号2059-authentication plugin…_zhanggonglalala的博客-CSDN博客_2059-authentication plugin
新建数据库
新建表
DROP TABLE IF EXISTS user;
CREATE TABLE user
(
id BIGINT(20) NOT NULL COMMENT '主键ID',
name VARCHAR(30) NULL DEFAULT NULL COMMENT '姓名',
age INT(11) NULL DEFAULT NULL COMMENT '年龄',
email VARCHAR(50) NULL DEFAULT NULL COMMENT '邮箱',
PRIMARY KEY (id)
);
新增数据记录,这里我们采用图形化的方式便捷的进行操作(点击菜单栏下面的+号)
从官网https://www.oracle.com/downloads/下载JDK。windows下可以选择exe版本或者zip版本,这里选用zip版本,解压后可以直接使用,下载并安装后,需要配置jdk环境变量。环境变量配置可以参考博客JDK的下载、安装和环境配置教程(2021年,win10)_Marvin_996_ICU的博客-CSDN博客_jdk。注意在配置环境变量之前如果打开了cmd,配置环境变量后新开一个cmd用于验证成功与否。
下载idea开发软件。idea官网一直进不去,参考网络上的博客修改DNS,host以及使用google浏览器都没有解决,最后通过切换WiFi为手机热点解决,可能是因为连接的网络问题导致无法访问。软件安装的具体过程可以参考博客:https://blog.csdn.net/weixin_43184774/article/details/100578786
建议java选择java8,springboot可以根据需求选择稳定版本(后面可以再更改)
修改springboot版本为2.2.1
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.1.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
同时我们参考博客对mavn换源。
修改idea自带maven的源_whojoe的博客-CSDN博客
引入相关依赖。
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
<!--mybatis-plus-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.0.5</version>
</dependency>
<!--mysql-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!--lombok用来简化实体类-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
下载lombok插件。
lombok这个插件可以简化bean的写法,对于get,set方法等可以不必再手写。
springboot2.1后的版本使用的jdbc驱动如下所示,对于的url中,serverTimelyZone表示的是时区。北京时间是东8区。
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/mybatis_plus?serverTimezone=GMT%2B8
spring.datasource.username=root
spring.datasource.password=123456
新建bean和mapper两个目录。
再entity中编写user,并使用lombok中的@Data注解代替手写getter,setter和构造器等。
@Data
public class User {
private Long id;
private String name;
private Integer age;
private String email;
}
使用快捷键alt+7可以快速看到User的目录结构。
在mapper包下新建UserMapper接口。
public interface UserMapper extends BaseMapper<User> {
}
在这个接口继承了BaseMapper接口,这个是MybatisPlus所提供的封装接口,为我们实现了Mapper的一些基础功能,我们可以不用再编写UserMapper.xml文件了。
public interface BaseMapper<T> {
int insert(T var1);
int deleteById(Serializable var1);
int deleteByMap(@Param("cm") Map<String, Object> var1);
int delete(@Param("ew") Wrapper<T> var1);
int deleteBatchIds(@Param("coll") Collection<? extends Serializable> var1);
int updateById(@Param("et") T var1);
int update(@Param("et") T var1, @Param("ew") Wrapper<T> var2);
T selectById(Serializable var1);
List<T> selectBatchIds(@Param("coll") Collection<? extends Serializable> var1);
List<T> selectByMap(@Param("cm") Map<String, Object> var1);
T selectOne(@Param("ew") Wrapper<T> var1);
Integer selectCount(@Param("ew") Wrapper<T> var1);
List<T> selectList(@Param("ew") Wrapper<T> var1);
List<Map<String, Object>> selectMaps(@Param("ew") Wrapper<T> var1);
List<Object> selectObjs(@Param("ew") Wrapper<T> var1);
IPage<T> selectPage(IPage<T> var1, @Param("ew") Wrapper<T> var2);
IPage<Map<String, Object>> selectMapsPage(IPage<T> var1, @Param("ew") Wrapper<T> var2);
}
如果读者在这里因为没有Mybatis基础,可能无法体会到Mybatis-Puls的优点,可以参考博客https://www.cnblogs.com/diffx/p/10611082.html学习。
在启动类Mpdemo1010Application中添加注解。
@MapperScan("package com.wangzhou.mpdemo1010.mapper")
在项目Test下有Mpdemo1010ApplicationTest.java,自动注入userMapper
@Autowired
private UserMapper userMapper;
由于userMapper是一个接口而不是类,这里会爆红,但是不影响运行,可以在UserMapper添加注解@repository解决。
编写一个findAll()测试方法。
@Test
public void findAll() {
List<User> users= userMapper.selectList(null);
System.out.println(users);
}
报错,从堆栈信息中看到Unknown database ‘mybatis_plus’,这是因为我们在application中配置存在问题,将mybtis_plus改为我们的建立的数据库名称‘mybatis-plus’
spring.datasource.url=jdbc:mysql://localhost:3306/mybatis-plus?serverTimezone=GMT%2B8
还是没有通过,错误信息提示
No qualifying bean of type 'com.wangzhou.mpdemo1010.mapper.UserMapper' available
这是因为主启动类中@MapperScan没有配置或者配置错误,配置如下:
@MapperScan("com.wangzhou.mpdemo1010.mapper")
成功打印出用户信息
[User(id=1, name=wz, age=18, email=wz@qq.com), User(id=2, name=wd, age=28, email=wd@qq.com)]
一般配置文件appliation.properties是叶子图标,如果不是可能是没有被idea识别,可能会导致错误。
这种情况的出现主要是由编译导致的。遇到这种情况可以把application.properties中的内容复制到target/classes目录下的同名文件中。
在application.properties中加上下面部分代码,可以查看被mp底层封装的sql。
#mybatis日志
mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
运行会打印
Creating a new SqlSession
SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@16d07cf3] was not registered for synchronization because synchronization is not active
JDBC Connection [HikariProxyConnection@1198531054 wrapping com.mysql.cj.jdbc.ConnectionImpl@2180e789] will not be managed by Spring
==> Preparing: SELECT id,name,age,email FROM user
==> Parameters:
<== Columns: id, name, age, email
<== Row: 1, wz, 18, wz@qq.com
<== Row: 2, wd, 28, wd@qq.com
<== Total: 2
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@16d07cf3]
@Test
public void insert() {
User user = new User();
user.setAge(19);
user.setEmail("pp@163.com");
user.setName("pp");
userMapper.insert(user);
System.out.println("insert:" + user);
}
运行结果如下:
==> Preparing: INSERT INTO user ( id, name, age, email ) VALUES ( ?, ?, ?, ? )
==> Parameters: 1428131805257474049(Long), pp(String), 19(Integer), pp@163.com(String)
<== Updates: 1
我们并没有设置id,也没有再mysql中设置其自动增长,但是mp帮我们生产了id(19位)。
自动增长策略:Auto Increment,分表情况下,每次生成新表还需得到上一张表最后一个值进行自动加1作为起始。
UUID:每次自动生成随机唯一的值。分表时生成新表无需得到上一张表的最后一个值,但这种方式无法排序。
Redis机制生成:redis是单线程的,因此也可以生成全局唯一ID,对主键id性能要求不高时,redis集群可以提高生成主键的吞吐量。
mp自带策略:snowflake算法
这些策略在实体类User中可以用@TableId注解设置。
@TableId(type = IdType.AUTO) //自增策略
private Long id;
其他的一些策略参考源码如下.
@Getter
public enum IdType {
/**
* 数据库ID自增
*/
AUTO(0),
/**
* 不设置主键生成策略
*/
NONE(1),
/**
* 用户输入ID
* 该类型可以通过自己注册自动填充插件进行填充
*/
INPUT(2),
/* 以下3种类型、只有当插入对象ID 为空,才自动填充。 */
/**
* 全局唯一ID (idWorker),mp自带策略,生成19位随机数,数字类型使用这种策略
*/
ID_WORKER(3),
/**
* 全局唯一ID (UUID)
*/
UUID(4),
/**
* 字符串全局唯一ID (idWorker 的字符串表示),mp自带策略,生成19位随机数,字符串类型使用这种策略
*/
ID_WORKER_STR(5);
private int key;
IdType(int key) {
this.key = key;
}
}
首先今天打开navicat遇到问题:2059 - authentication plugin ‘caching_sha2_password’,参考以下链接解决。
https://jingyan.baidu.com/article/0aa22375e7966ac8cc0d64b3.html
这个问题我之前其实已经遇到过,解决过一次,为什么又重新出现了呢?当我进入数据库,发现另外一件事情,就是我的mysql里没有之前创建的数据库:mybatis-plus了。
运行之前编写的测试用例:
@Test
public void findAll() {
List<User> users= userMapper.selectList(null);
System.out.println(users);
}
结果不能通过:报错。
java.sql.SQLSyntaxErrorException: Unknown database 'mybatis-plus'
突然想起自己服务启动的是mysql80,在电脑里找到服务,停掉mysql80,并将它设置为手动启动,启动mysql。
检查navicat,终于出现了我可爱的mybatis-plus,在idea里跑之前写的测试案例也能够通过了。
查找资料得知,原来mysql和mysql80是两个服务,mysql80是安装包解压后自动安装的,mysql是用户初始化后手动安装的,二者不能同时启动。
先回顾下mysql中update语句的写法。
update user set age = 20 where id = 1;
在mybatisplus中简单的sql语句可以不用自己写了,直接调用userMapper对应的方法即可。
@Test
public void update() {
User user = new User();
user.setId(1l);
user.setAge(20);
//返回更新的行数
int row = userMapper.updateById(user);
System.out.println(row);
}
点击navicat对应表页面的左下角倒数第二个图标刷新.
查看结果,更新操作成功了。
在表中新增字段
在实体类user中新增对应字段,并添加注解,注意mysql中字段使用的是下划线风格,java中使用的是驼峰规则。
@TableField(fill = FieldFill.INSERT)
private Date createTime;
@TableField(fill = FieldFill.INSERT_UPDATE)
private Date updateTime;
相关的配置可以参考官方文档https://mp.baomidou.com/guide/auto-fill-metainfo.html。
创建一个类,实现接口MetaObjectHandler。
@Component // @Component注解将MyMetaObjectHandler交给spring管理
public class MyMetaObjectHandler implements MetaObjectHandler {
// 使用mp执行insert操作,这个方法会自动执行
@Override
public void insertFill(MetaObject metaObject) {
// 根据这个方法的名字即可知道对应功能。第一个参数filed是要填充的字段,第二个参数val是填充的值,第三个参数是元数据
// 元数据是指描述其他数据的数据:表名,字段名(id,name...)等
this.setFieldValByName("createTime", new Date(), metaObject);
this.setFieldValByName("updateTime", new Date(), metaObject);
}
// 使用mp执行update操作。这个方法会自动执行
@Override
public void updateFill(MetaObject metaObject) {
this.setFieldValByName("updateTime", new Date(), metaObject);
}
}
执行测试案例,但是发现方法一直在执行中,无法完成执行。但是这个奇奇怪怪的问题可能和网络有关系,当我回到家中用家里的WiFi测试就没有这个问题了。
但是执行update()时报错。
Error updating database. Cause: java.sql.SQLException: Field 'id' doesn't have a default value
这是因为mysq中没有设置主键自增。在navicat中进行设置,确认保存修改,重新测试通过。
如果不考虑事务的隔离性,读操作会有三个问题:脏读(读到了未)、幻读(重点在于新增或者删除:在同一事务中,同样的条件,第一次和第二次读出来的记录数不一样)、不可重复读(重点是修改:在同一事务中,同样的条件,第一次读的数据和第二次读的数据不一样)。如果不考虑事务的隔离性,写操作可能会导致丢失更新数据问题,乐观锁是为了解决丢失更新问题而设计的。
所谓丢失更新数据是指在并发操作下,后提交的事务对数据的更新会覆盖先提交的事务的数据。解决丢失数据更新问题主要有两个方法:悲观锁和乐观锁,悲观锁是指同一时间只允许一个事务对同一共享数据执行操作。乐观锁是指提供一个版本号,每个事务执行数据更新之前先比较当前数据版本与数据库版本是否一致,如果一致则执行更新并将数据库版本号+1,如果不一致则不进行更新。
乐观锁一个实际的应用就是抢票功能,假设余票数为1,抢票用户为100,100个用户都可以点击看到这张余票并进行抢票,但是最终只有第一个付款成功的乘客可以抢到票。
在mybatis中实现乐观锁的步骤如下:
1.在数据库中对表user添加version字段。
2.在实体类User中添加属性version,并添加@Version注解
@Version
private Integer version;
3.新建config包,包下建立MpConfig类,用于统一管理配置信息。在Mpconfig中添加mp拦截器插件的代码。并把启动类中的配置信息移植到该类进行统一管理。
@Configuration
@MapperScan("com.wangzhou.mpdemo1010.mapper")
public class MpConfig {
// 乐观锁插件
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
return interceptor;
}
}
4.(非必须)在实体类user中的version添加注解。
@TableField(fill = FieldFill.INSERT)
并在MyMetaObjectHandler的insertFill()中添加如下代码,设置version初始值为1.
this.setFieldValByName("version", 1, metaObject);
编写测试代码测试,注意这里需要先查询,再修改,因为只有通过查询拿到version版本号才能够再update时进行版本对比。
@Test
public void testOptimisticLocker() {
//先查询
User user = this.userMapper.selectById(1428131805257474051l);
//后修改
user.setAge(99);
this.userMapper.updateById(user);
}
简单的查询如下:
//单个id查询
@Test
public void testSelectDemo() {
User user = this.userMapper.selectById(1l);
System.out.println(user);
}
//多个id查询
@Test
public void testSelectDemo1() {
List users = this.userMapper.selectBatchIds(Arrays.asList(1l, 2l, 3l));
System.out.println(users);
}
//根据条件查询
@Test
public void testSelectDemo2() {
Map map = new HashMap<String, Object>();
map.put("name", "独孤求败");
map.put("age" , 19);
this.userMapper.selectByMap(map);
}
分页查询的操作步骤是:
1.配置分页插件(在启动类或者怕配置类中,推荐配置类)
// 在新版mp中已过时
// @Bean
// public PaginationInterceptor paginationInterceptor() {
// return new PaginationInterceptor();
// }
//分页插件
@Bean
public MybatisPlusInterceptor paginationInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.H2));
return interceptor;
}
2.new一个page对象,传入当前页、每页显示记录数。调用mp中的分页方法。
//分页查询
@Test
public void testPage() {
Page <User> page = new Page<>(1, 3);
// mp底层会把分页查询到的结果封装到page中
this.userMapper.selectPage(page, null);
System.out.println("页数:" + page.getPages());
System.out.println("当前页:" + page.getCurrent());
System.out.println("当前数据的List集合:" + page.getRecords());
System.out.println("每页记录数:" + page.getSize());
System.out.println("记录总数:" + page.getTotal());
System.out.println("是否有下一页:" + page.hasNext());
System.out.println("是否有上一页:" + page.hasPrevious());
}
逻辑删除是指表中数据仍然存在,但是在查询时将不会再查询到。可以通过添加标志位区分已删除元素和未删除元素,实现逻辑删除。逻辑删除可以恢复数据,相对于物理删除,应用的更为频繁。
//物理删除
@Test
public void testDelete() {
int result = this.userMapper.deleteById(1l);
System.out.println(result);
}
//批量删除
@Test
public void testBatchDelete() {
int result = this.userMapper.deleteBatchIds(Arrays.asList(2l, 3l));
System.out.println(result);
}
逻辑删除的步骤是:
1.表中添加字段deleted(设置默认值为0),在实体类中添加属性。
//注解表示其用于逻辑删除
@TableLogic
private Integer deleted;
2.配置逻辑删除插件
事实上,这里可以不要配置,默认会置删除为1,未删除为0,当然,也可以按照自己的规则进行配置。
#设置逻辑删除中删除与未删除的默认值
mybatis-plus.global-config.db-config.logic-delete-value=1
mybatis-plus.global-config.db-config.logic-not-delete-value=0
3.执行删除操作
执行一条新的insert语句(这个数据的deleted字段会被自动填充为0),此时再执行删除操作时底层的sql如下:
UPDATE user SET deleted=1 WHERE id=? AND deleted=0
查看表中数据,删除的数据还在,且deleted字段置为1了。
逻辑删除配置后对于操作的说明如下。
说明:
只对自动注入的sql起效:
插入: 不作限制
查找: 追加where条件过滤掉已删除数据,且使用 wrapper.entity 生成的where条件会忽略该字段
更新: 追加where条件防止更新到已删除数据,且使用 wrapper.entity 生成的where条件会忽略该字段
删除: 转变为 更新
例如:
删除: update user set deleted=1 where id = 1 and deleted=0
查找: select id,name,deleted from user where deleted=0
字段类型支持说明:
支持所有数据类型(推荐使用 Integer,Boolean,LocalDateTime)
如果数据库字段使用datetime,逻辑未删除值和已删除值支持配置为字符串null,另一个值支持配置为函数来获取值如now()
附录:
逻辑删除是为了方便数据恢复和保护数据本身价值等等的一种方案,但实际就是删除。
如果你需要频繁查出来看就不应使用逻辑删除,而是以一个状态去表示。
在config中配置性能分析插件
//性能分析插件
/**
* SQL 执行性能分析插件
* 开发环境使用,线上不推荐。
*/
@Bean
@Profile({"dev","test"})// 设置 dev test 环境开启
public MybatisPlusInterceptor performanceInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new IllegalSQLInnerInterceptor());
return interceptor;
}
在application.properties中设置环境
# 环境设置为dev
spring.profiles.active=dev
遗憾的是,在新的mybatis-plus中该插件已经被移除了。
我们一般使用QueryWrapper
来实现在mp中的复杂条件查询。
//复杂查询操作
@Test
public void testSelectQuery() {
// user whose age>=30
QueryWrapper wrapper = new QueryWrapper<User>();
wrapper.ge("age", 30);
List<User> users = userMapper.selectList(wrapper);
for(User user : users) {
System.out.println(user);
}
// user whose name.equals("风清扬")
QueryWrapper wrapper2 = new QueryWrapper<User>();
wrappe2r.eq("name","风清扬");
List<User> users2 = userMapper.selectList(wrapper2);
for(User user : users2) {
System.out.println(user);
}
System.out.println("-----------");
QueryWrapper wrapper3 = new QueryWrapper<User>();
// user whose name have character “岳”
wrapper3.like("name","岳");
List<User> users3 = userMapper.selectList(wrapper3);
for(User user : users3) {
System.out.println(user);
}
System.out.println("-----------");
QueryWrapper wrapper4 = new QueryWrapper<User>();
// users order by id asc
wrapper4.orderByAsc("id");
List<User> users4 = userMapper.selectList(wrapper4);
for(User user : users4) {
System.out.println(user);
}
System.out.println("-----------");
QueryWrapper wrapper5 = new QueryWrapper<User>();
// splicing sql
wrapper5.last("limit 2");
List<User> users5 = userMapper.selectList(wrapper5);
for(User user : users5) {
System.out.println(user);
}
System.out.println("-----------");
QueryWrapper wrapper6 = new QueryWrapper<User>();
// assign the content selected
wrapper6.select("id", "name");
List<User> users6 = userMapper.selectList(wrapper6);
for(User user : users6) {
System.out.println(user);
}
}