Spring Boot 数据访问与事务

目录

JdbcTemplate

JPA

Spring Boot集成Mybatis-Plus

基本用法

分页

条件查询

物理删除和逻辑删除

多表联查

事务

事务的定义与特性

事务的并发问题

事务隔离级别详解:

事务的并发问题

1. 脏读:

2. 不可重复读

3. 幻读

4. 丢失更新

a. 第一类丢失更新例子

b. 第二类丢失更新例子

编程式事务和声明式事务

Spring事务的传播行为


JdbcTemplate

需要引入依赖

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
  <groupId>mysql</groupId>
  <artifactId>mysql-connector-java</artifactId>
</dependency>
@Repository
public class StudentDaoImpl implements StudentDao {
    @Resource
    private  JdbcTemplate jdbcTemplate;

    @Override
    public List<Student> listStudent() {
        List<Student> students = jdbcTemplate.query("select * from student", new BeanPropertyRowMapper<>(Student.class));
        return students; 
    }

    @Override
    public int addStudent(Student student) {
        String sql ="insert into student (username,password,sex,address) values (?,?,?,?)";

        return jdbcTemplate.update(sql,student.getUsername(),student.getPassword(),student.getSex(),student.getAddress());
    }

    @Override
    public int deleteStudent(Integer id) {
        String sql = "delete from student where id = ?";
        int row = jdbcTemplate.update(sql, id);
        return row;
    }

    @Override
    public int updateStudent(Student student) {
        String sql = "update student set username = ?,password = ?, address = ? where id = ?";
        int row = jdbcTemplate.update(sql, student.getUsername(), student.getPassword(), student.getAddress(), student.getId());
        return row;
    }

    @Override
    public Student getStudentById(Integer id) {
        Student student = jdbcTemplate.queryForObject("select * from student where id = "+id,new BeanPropertyRowMapper<>(Student.class));
        return student;
    }

new BeanPropertyRowMapper<>(实体类.class):是Spring Jdbc模块中的一个实体类,用于将数据库查询结果映射到java对象的属性上

JPA

JPA (Java Persistence API)是由Sun官方提出的Java持久化规范,并不是一套产品。它为java开发人员提供一种对象/关联映射工具来管理java应用中的关系数据。他的出现主要是为了简化现有的持久化开发工作和整合0RM技术,结束现在Hibernate、TopLink、JD0等0RM框架各自为营的局面。

JPA在充分吸收现有的Hibernate、TopLink、 JD0等0RM框架的基础上发展而来的,具有易于使用,伸缩性强等优点。Spring data JPA是Spring基于PRM框架、JPA规范基础上封装的一套JPA应用框架,底层使用了Hibernate的JPA技术实现,可使开发者用极简的代码实现对数据库的访问和操作。

  • 引入依赖
<dependency>
  <groupId>mysql</groupId>
  <artifactId>mysql-connector-java</artifactId>
  <scope>runtime</scope>
</dependency>
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
  • 配置
#开启根据实体类结构自动创建表
#简化后:jpa.hibernate.ddl-auto=update
spring.jpa.properties.hibernate.hbm2ddl.auto=update
#指定sqL语句的使用版本
spring.jpa.properties.hibernate.hbm2ddl.dialect=org.hibernate.dialect.MySQL5InnoDBDialect
#是否显示sql语句在控制台
#jpa.show-sql=true
spring.jpa.show-sql=true
#指定Hibernate使用的标准物理命名策略
jpa.hibernate.naming.physical-strategy=org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
  • 实体类
@Data //自动生成getter,setter方法
@Table(name = "tb_book")  
@Entity
@Accessors(chain = true)
public class Book {
    @Id//主键
    @GeneratedValue(strategy = GenerationType.IDENTITY)//自增生成id
    @Column(nullable =false )//不为空
    private Integer id;


    private String bookName;
    @Column
    private String author;
}

@Table:用于指定实体类和数据库表之间的映射关系

@Id:用于指定实体类的主键属性,对应于数据库表中的主键列

@Column:用于映射实体类的属性与数据库表的列,通过@Column注解,你可以指定实体类属性在数据库中对应的列的属性和约束

@Entity:用于将java类声明为JPA实体类,通过JPA,可以通过实体类来进行数据库的CRUD操作

@Accessors:通常与Lombok一起使用,用于配置生成的getter,setter方法的风格
@GeneratedValue:指定实体类主键的生成策略

  • dao层
public interface BookDao extends JpaRepository<Book,Integer> {

}
  • 测试类
@SpringBootTest
class Ch6JpaApplicationTests {
    @Autowired
    private BookDao bookDao;



    @Test
    void list() {

        //加一个排序 -根据ID降序排
        List<Book> bookList = bookDao.findAll(Sort.by("id").descending());
        bookList.forEach(b -> System.out.println(b));
    }
    @Test
    void findById() {
        Optional<Book> byId = bookDao.findById(1);
        Book book = byId.get();
        System.out.println(book);
    }
    @Test
    void save() {
        Book book = bookDao.save(new Book().setBookName("西游记").setAuthor("吴承恩"));
        System.out.println(book);
    }
    @Test
    void delete() {
        bookDao.delete(new Book().setId(3));

    }
    //修改
    @Test
    void update() {
        Book book = bookDao.save(new Book().setId(1).setBookName("红楼梦").setAuthor("曹雪芹"));
        System.out.println(book);

    }
}

spring.jpa.properties.hibernate.hbm2ddl.auto配置如下:

JPA自定义方法命名规则

Spring Boot集成Mybatis-Plus

基本用法

  • pom引入依赖
<dependency>
  <groupId>mysql</groupId>
  <artifactId>mysql-connector-java</artifactId>
  <scope>runtime</scope>
</dependency>
<dependency>
  <groupId>com.baomidou</groupId>
  <artifactId>mybatis-plus-boot-starter</artifactId>
  <version>3.4.2</version>
</dependency>
  • dao接口
@Mapper
public interface AddressDao extends BaseMapper<Address.class>{}
  • 配置文件
spring.datasource.url=jdbc:mysql:///springdata-test?characterEncoding=utf8&serverTimezone=Asia/Shanghai
spring.datasource.username=root
spring.datasource.password=123456
#控制台打印sql
#logging.level.com.xiaojie.ch6mybatisplus.dao=debug
#控制台打印sql语句的另外一种写法
mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
#驼峰命名——默认是开启的true
mybatis-plus.configuration.map-underscore-to-camel-case=false
  • 测试
@Test
    void contextLoads() {
        List<Address> students = addressMapper.selectList(null);
        students.forEach(s-> System.out.println(s));
    }
    @Test
    void add(){
        Address address = new Address().setCity("南京").setArea("bb").setProvince("江苏").setDetailedAddress("xxxx");
        int insert = addressMapper.insert(address);
        System.out.println(insert);
        System.out.println(address.getId());
    }
  • .插入数据的时候注意

Mybatis-puls默认添加添加对象时,采用分布式唯一 ID, 根据雪花算法得出长度为19的唯一ID插入表中,则不建议表中Id为int类型。

解决方案1:将表ID改为VARCHAR类型,实体类改为String类型,采用雪花算法,并且可以立即获得新增后的最新雪花Id。

解决方案2:不采用雪花算法生成Id,实体类添加@TableId注解指定ID生成策略,即可存入自增的ID

public class Address {
    @TableId(type = IdType.AUTO)
    private String id;

雪花算法介绍:

snowflake是Twitter开源的分布式ID生成算法,结果是一个long型的ID。 其核心思想是:使用41bit作为毫秒数,10bit作为机器的ID (5个bit是数据中心,5个bit的机器ID), 12bit作为毫秒内的流水号(意味着每个节点在每毫秒可以产生4096个ID),最后还有一个符号位,永远是0。具体实现的代码可以参看https://github.com/twitter/snowflake.雪花算法支持的TPS可以达到419万左右(2^12*1000) 几乎保证全球唯一。

分页

在配置类中添加分页的拦截器并设置数据库类型(MySql)

@Configuration
public class PageConfig {
    @Bean
    MybatisPlusInterceptor mybatisPlusInterceptor (){
        MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
        mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
        return mybatisPlusInterceptor;
    }
}
@Test
    void testPage(int pageNo,int pageSize){
        Page<Address> page = new Page<>(pageNo,pageSize);//分页,
        page.addOrder(OrderItem.desc("id"));//加个排序  降序
        //null的位置为需要条件查询时,放入的QueryWrapper对象,此处只需要分页查询,故而使用null顶替
        Page<Address> page1 = addressMapper.selectPage(page, null);
        System.out.println(page1.getRecords());  //输出查询的内容
    }

条件查询

  1. Map的方式进行条件查询
@Test
void testMap(){
    Map map = new HashMap();
    map.put("province","湖南省");
    List list = addressMapper.selectByMap(map);
    list.forEach(System.out::println);
}
  1. QueryWrapper的方式
     @Test
    void testSelectBy(){
        //查询ID在某个范围,且名字城市名字左模糊匹配,只赋值id字段
        QueryWrapper<Address> queryWrapper1 = new QueryWrapper();
        queryWrapper1.between("id",3,4)
                .like("city","岳阳")
                .select("id");


        List<Address> list = addressMapper.selectList(queryWrapper1);
        list.forEach(System.out::println);
    }
  1. LambdaQueryWrapper

分页带条件查询

 @GetMapping("/get")
    public Page<SysUser> getList(SysUser user, int pageNo, int pageSize) {
        LambdaQueryWrapper<SysUser> lambdaQueryWrapper = new LambdaQueryWrapper<>();
     //like()中,第一个参数为判断getRealName是否为空,
     //第二个参数为调用实体类中RealName的属性名,第三个参数为需要查询的值
        lambdaQueryWrapper.like(StringUtils.hasText(user.getRealName()), SysUser::getRealName, user.getRealName());
        lambdaQueryWrapper.eq(user.getRoleId() != null, SysUser::getRoleId, user.getRoleId());
         //第一个参数为分页信息,第二个参数为条件查询对象
        Page<SysUser> page = sysUserService.page(new Page<>(pageNo, pageSize), lambdaQueryWrapper);
        return page;
    }

querywrapper查询条件构造器

物理删除和逻辑删除

Mybatis-plus物理删除:在数据库中移除。逻辑删除:数据库中没有移除,而是在代码中使用一个变量来使他失效! (如: delete=0=>delete=1;)

方案:给数据表增加一个字段:is_del,用于表示该数据是否被逻辑删除,初始值为 0。0 表示未删除, 1 表示删除。

1.方法一

  • 实体类字段上加上注解
  • value = “未删除的值,默认值为0”;若设置为2,则查询时 where 后面自动拼接 is_del = 2
  • delval = “删除后的值,默认值为1”
@TableField(fill = FieldFill.INSERT)
@ApiModelProperty("是否删除标识 0:未删除  1:删除")
@TableLogic(value = "0",delval = "1")
private int isDel;

执行删除,原 deleteById 会物理删除,现在实现逻辑删除

testMapper.deleteById(id);

:不可以,如需改变最后修改时间,请选择方法二

2.方法二

思路就是通过 update 方法来把 is_del 从 0 更新为 1,通过 MyBatis Plus 提供的自动填充功能,可以自动更新最后修改时间和最后修改人

配置自动填充

import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import org.apache.commons.lang3.StringUtils;
import org.apache.ibatis.reflection.MetaObject;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;

@Component
public class MyObjectHandler implements MetaObjectHandler {

    @Override
    public void insertFill(MetaObject metaObject) {
        //属性名称,不是字段名称
        this.setFieldValByName("CreateTime", new Date(), metaObject);
        this.setFieldValByName("ModifiedTime", new Date(), metaObject);
    }

    @Override
    public void updateFill(MetaObject metaObject) {
        this.setFieldValByName("ModifiedTime", new Date(), metaObject);
    }
}
 @ApiModelProperty(value = "创建时间")
    @TableField(fill = FieldFill.INSERT)
    private Date CreateTime;

    @ApiModelProperty(value = "更新时间")
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private Date ModifiedTime;

service 层执行,找出 id 对应的一行,然后把 is_del 字段更新成 1

    public void deleteById(Long id) {
        LambdaUpdateWrapper<TestPO> updateWrapper = new UpdateWrapper<TestPO>().lambda();
        updateWrapper.set(TestPO::getIsDel, 1).eq(TestPO::getId, id);
        baseMapper.update(new TestPO(), updateWrapper);
    }

多表联查

  • 传统的方式:一旦涉及到多表查询就改用mybatis的sql映射文件的方式
  • mybatis-plus-join工具

参考文章:MyBatis-Plus联表查询的短板,终于有一款工具补齐了

事务

事务的定义与特性

  1. 事务的定义

数据库事务可以包含一个或者多个数据库操作,但这些操作构成一个逻辑上的整体,这个逻辑整体中的数据库操作,要么全部执行成功,要么全部不执行,也就是说构成事务的所有操作,要么全都对数据产生影响,要么全都不产生影响,不管事务是否执行成功,数据库总是保持一致性的状态。

  1. 事务的特性

事务的特性:原子性(Atomicity),一致性(Consistency),隔离性(Isolation),持久性(Durability),简称ACID

    • 原子性:事务所有操作作为一个整体,像原子一样不可分割。要么全部成功,要么全部失败
    • 一致性:事务的执行结果必须使数据库从一个一致性的状态到另一个一致性的状态。一致性的状态是指系统的状态满足数据完整性约束(主码,参照完整性,check约束等),并且系统的状态反应数据库本应描述的显示的真实状态,比如银行转账之后,互相转账的两个账户金额总和保持不变。
    • 隔离性:并发执行的事务不会相互影响,其对数据库的影响和它们串行执行时一样,比如多个用户同时往一个账户转账,最后账户的结果应该和他们按先后次序转账的结果一样。
    • 持久性:事务一旦提交,其对数据库的更新就是持久的,任何事务或系统故障都不会导致数据丢失,不会因为系统故障或者断电造成数据不一致或者丢失。

事务的并发问题

事务隔离级别详解:

mysql中,对于隔离级别的操作命令:

查询数据库的隔离级别:
select @@transaction_isolation;

切换隔离级别
set session transaction_isolation='read-uncommitted';
set session transaction_isolation='read-committed';
set session transaction_isolation='repeatable-read';
set session transaction_isolation='serializable';

事务的并发问题

一个数据库中的一份数据,由于被多客户端并发访问,或者被多线程并发访问,如果没有采取必要的隔离措施,就可能导致一些问题:比如脏读不可重复读幻读丢失更新

事务隔离级别

标记

可能导致的并发异常

脏读

不可重复读

幻读

丢失更新

读未提交

READ-UNCOMMITTED

可能

可能

可能

可能

读已提交

READ-COMMITTED

可能

可能

可能

可重复读

REPEATABLE-READ

可能

串行化

SERIALIZABLE

1. 脏读:

事务A读取了事务B更新的数据,然后B回滚操作,那么A读取到的数据是脏数据

时间节点

事务A

事务B

T1

开始事务

T2

开始事务

T3

查询账户余额为500元

T4

存入1000元 ,成为vip

T5

查询账户余额为1500元(脏读)给发了100优惠券

T6

事务回滚(余额恢复为500元)

T7

提交

此案例导致发放的100元优惠卷无法收回(回滚)

2. 不可重复读

一个事务中两次(不同时间点)读取同一行数据,但是这两次读取到的数据不一致

时间节点

取款事务A

查询事务B

T1

开始事务

T2

开始事务

T3

查询账户余额为500

T4

查询账户余额500

T5

取出200(余额更新为300元)

T6

提交事务

T7

查询账户余额为300元

T8

提交事务

在这个场景中,事务B在事务A取款前后读取的账户余额不一致。

造成的危害:假设b事务是一个很大的事务,a事务是一个很小的事务(这种小事务可能很多),那么b事务的程序里面使用数据库中的变量进行判断时,每次读的东西都不一样,导致程序不知到底该怎样。

不符合事务特性中的隔离性,各个事务之间应该互不影响

3. 幻读

T1读取了一个字段,T2对字段进行了插入。此时T1再次读取时仍然为原来的数据,不过T1如果对T2插入行同样进行插入时就会报错。

时间节点

事务 A

事务 B

T1

开始事务

T2

查询所有用户(假设没有id为2的)

T3

开始事务

T4

(捷足先登)新增一个用户

T5

提交事务

T6

新增一个用户id为2失败(幻读)

可以理解成:幻读是不可重复读的一种特殊场景

4. 丢失更新

丢失更新又可细分为第一类丢失更新第二类丢失更新.事务A覆盖了事务B已提交的更新数据,导致事务B的更新数据好像丢失了一样,成为丢失更新

a. 第一类丢失更新例子

时间节点

事务 A

事务 B

T1

开始事务

T2

开始事务

T3

查询账户余额为500元

T4

查询账户余额为500元

T5

取款100元(余额400元)

T6

存款200元(余额700元)

T7

提交事务

T8

事务回滚

余额

应有余额

实际余额

500元

400元

在这个场景中,我们会发现事务A发生回滚后事务B的操作丢失了,这种数据丢失会导致严重的问题,上述场景中的个人账户就缺少了100元

b. 第二类丢失更新例子

时间节点

事务 A

事务 B

T1

开始事务

T2

开始事务

T3

查询账户余额为500元

T4

查询账户余额为500元

T5

取款100元(余额400元)

T6

存款200元(余额700元)

T7

提交事务

T8

提交事务

余额

应有余额

实际余额

600元

700元

在这个场景中,我们会发现事务A发生回滚后事务B的操作丢失了,这种数据丢失会导致严重的问题,上述场景中的银行层面账户就缺少了100元

幻读与不可重复读很类似,都是读取到不一致的数据,当然本质上也是有区别的.幻读的侧重点在于插入和删除,即二次查询数据会比第一次查询数据变多或者变少.不可重复读的侧重点在于修改,即第二次查询与第一次查询的同一条记录中某些字段不一致的情况

编程式事务和声明式事务

@Autowired
private BankMapper bankMapper;


@Autowired
private TransactionTemplate transactionTemplate;

@Override
@Transactional
public boolean transfer(Integer staffId, Integer fromCustomerId, Integer toCustomerId, BigDecimal money) {
    BankCustomer fromCustomer = bankMapper.selectBankCustomerById(fromCustomerId);
    BankCustomer toCustomer = bankMapper.selectBankCustomerById(toCustomerId);
    //第一步:给转账客户扣钱
    bankMapper.updateBankAccountBalance(fromCustomer.getBankAccount().getId(),money.negate());

    //模拟交易过程中发生异常
    //int a = 1/0;

    //第二步:记录转账客户流水
    BankOperate fromOperate = new BankOperate();
    fromOperate.setCustomerId(fromCustomerId);
    fromOperate.setStaffId(staffId);
    fromOperate.setType(1);
    fromOperate.setLastBalance(fromCustomer.getBankAccount().getBalance());
    fromOperate.setBalance(fromCustomer.getBankAccount().getBalance().subtract(money));
    fromOperate.setMoney(money);
    Date date1 = new Date();
    fromOperate.setCreateTime(date1);
    fromOperate.setUpdateTime(date1);
    bankMapper.insertBankOperate(fromOperate);

    //第三步:给被转账客户加钱
    bankMapper.updateBankAccountBalance(toCustomer.getBankAccount().getId(),money);

    //第四步:记录被转账客户流水
    BankOperate toOperate = new BankOperate();
    toOperate.setCustomerId(toCustomerId);
    toOperate.setStaffId(staffId);
    toOperate.setType(0);
    toOperate.setLastBalance(toCustomer.getBankAccount().getBalance());
    toOperate.setBalance(toCustomer.getBankAccount().getBalance().add(money));
    toOperate.setMoney(money);
    Date date2 = new Date();
    toOperate.setCreateTime(date2);
    toOperate.setUpdateTime(date2);
    bankMapper.insertBankOperate(toOperate);

    return true;





    //编程式事务
    /*return transactionTemplate.execute(new TransactionCallback<Boolean>() {
            @Override
            public Boolean doInTransaction(TransactionStatus transactionStatus) {
                try {
                    //第一步:给转账客户扣钱
                    bankMapper.updateBankAccountBalance(fromCustomer.getBankAccount().getId(),money.negate());

                    //模拟交易过程中发生异常
                    //int a = 1/0;

                    //第二步:记录转账客户流水
                    BankOperate fromOperate = new BankOperate();
                    fromOperate.setCustomerId(fromCustomerId);
                    fromOperate.setStaffId(staffId);
                    fromOperate.setType(1);
                    fromOperate.setLastBalance(fromCustomer.getBankAccount().getBalance());
                    fromOperate.setBalance(fromCustomer.getBankAccount().getBalance().subtract(money));
                    fromOperate.setMoney(money);
                    Date date1 = new Date();
                    fromOperate.setCreateTime(date1);
                    fromOperate.setUpdateTime(date1);
                    bankMapper.insertBankOperate(fromOperate);

                    //第三步:给被转账客户加钱
                    bankMapper.updateBankAccountBalance(toCustomer.getBankAccount().getId(),money);

                    //第四步:记录被转账客户流水
                    BankOperate toOperate = new BankOperate();
                    toOperate.setCustomerId(toCustomerId);
                    toOperate.setStaffId(staffId);
                    toOperate.setType(0);
                    toOperate.setLastBalance(toCustomer.getBankAccount().getBalance());
                    toOperate.setBalance(toCustomer.getBankAccount().getBalance().add(money));
                    toOperate.setMoney(money);
                    Date date2 = new Date();
                    toOperate.setCreateTime(date2);
                    toOperate.setUpdateTime(date2);
                    bankMapper.insertBankOperate(toOperate);

                    return true;
                } catch (RuntimeException e) {
                    // 遇到运行时异常就回滚
                    transactionStatus.setRollbackOnly();
                    return false;
                }
            }
        });*/

    }

Spring事务的传播行为

参考网址:https://blog.csdn.net/qq_38262266/article/details/108709840

https://www.jf3q.com/article/detail/4660

参考视屏:https://www.bilibili.com/video/BV1R8411c7m2

  • 15
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值