sharp-database使用指北

简介

sharp-database 是一个持久化操作工具的java实现,主要做查询功能。底层依赖 JdbcTemplate 。可以结合 MyBatis 的动态SQL一起使用。sharp-database 作为数据库操作工具的补充,不是用来了替换 MyBatisJPA。目前支持 MysqlOracle 两种数据库,默认支持 Mysql

pom.xml 添加依赖

<dependency>
    <groupId>com.rick.db</groupId>
    <artifactId>sharp-database</artifactId>
    <version>1.0-SNAPSHOT</version>
</dependency>

application.xml

sharp:
  database:
    type: oracle

方法测试

SharpService

获取Map集合

List<Map<String, Object>> query(String sql, Map<String, ?> params)

测试代码

@Test
public void testListAsMap() {
    // language=SQL
    String sql = "SELECT id, title, description, create_time, is_delete, create_by\n" +
            "FROM t_project\n" +
            "WHERE id IN (:id)\n" +
            "  AND title LIKE :title\n" +
            "  AND create_time > :createTime\n" +
            "  AND is_delete = :deleted" +
            "  AND create_by = ${createBy}";

    Map<String, Object> params = new HashMap<>();
//        params.put("id", "1341369230607347714,1341299740150394882,1341734037772668930,1341313565406904322,1341368363682439169");
//        params.put("id", Arrays.asList(1341369230607347714L,
//                1341299740150394882L,1341734037772668930L,1341313565406904322L,1341368363682439169L));
//        params.put("id", Sets.newHashSet(1341369230607347714L,
//                1341299740150394882L, 1341734037772668930L, 1341313565406904322L, 1341368363682439169L));
    params.put("id", new Long[] { 1341369230607347714L,
            1341299740150394882L, 1341734037772668930L, 1341313565406904322L, 1341368363682439169L});

    params.put("title", "haha");
    params.put("createTime", "2020-10-22 17:27:41");
    params.put("createBy", "156629745675451");

    List<Map<String, Object>> list = gridService.query(sql, params);
    list.forEach(System.out::println);
}

SQL打印

SELECT id, title, description, create_time, is_delete
FROM t_project
WHERE id IN (:id0, :id1, :id2, :id3, :id4)
  AND UPPER(title) LIKE CONCAT('%', UPPER(:title), '%') ESCAPE '\\'
    AND create_time > :createTime

args:=> [{id0=1341734037772668930, createTime=2020-10-22 17:27:41, id2=1341313565406904322, id1=1341368363682439169, id4=1341369230607347714, id3=1341299740150394882, title=haha, create_by=156629745675451}]    

代码中SQL有5个变量:id、title、createTime、deleted、create_by。但是params中只提供了id、title、createTime、create_by4个参数。在执行查询的时候会忽略没有提供参数的变量。类似于 Mybatis 中的动态SQL。只是这里不需要通过if去手动判断。
常见的变量形式:

  • id = :id
  • id in (:id) 值可以是List Set 等实现了 Iterable接口的对象;可以是数组,如new Long[] {1, 2};或者是以逗号分割的字符串,如“1341369230607347714,1341299740150394882”
  • title like :title 不区分大小写,%:title%
  • create_time > :createTime
  • id = ${id} 这个是替换操作,会有sql注入的风险

获取对象集合

public <T> List<T> query(String sql, Map<String, ?> params, Class<T> clazz)

测试代码

@Test
public void testListAsClass() {
    // language=SQL
    String sql = "SELECT id, title, description, create_time, is_delete, create_by\n" +
            "FROM t_project\n" +
            "WHERE id IN (:id)\n" +
            "  AND title LIKE :title\n" +
            "  AND create_time > :createTime\n" +
            "  AND is_delete = :deleted" +
            "  AND create_by = ${createBy}";

    Map<String, Object> params = new HashMap<>();
    params.put("id", new Long[] { 1341369230607347714L,1341299740150394882L});

    List<Project> list = gridService.query(sql, params, Project.class);
    list.forEach(System.out::println);
}

获取自定义的集合

public <T> List<T> query(String sql, Map<String, ?> params, JdbcTemplateCallback<T> jdbcTemplateCallback)

测试代码

@Test
public void testListAsCustom() {
    // language=SQL
    String sql = "SELECT id, title, description, create_time, is_delete, create_by\n" +
            "FROM t_project\n" +
            "WHERE id IN (:id)\n" +
            "  AND title LIKE :title\n" +
            "  AND create_time > :createTime\n" +
            "  AND is_delete = :deleted" +
            "  AND create_by = ${createBy}";

    Map<String, Object> params = new HashMap<>();
    params.put("title", "title");

    List<String> list = gridService.query(sql, params, new SharpService.JdbcTemplateCallback<String>() {
        @Override
        public List<String> query(NamedParameterJdbcTemplate jdbcTemplate, String sql, Map<String, ?> paramMap) {
            return jdbcTemplate.query(sql, paramMap, new RowMapper<String>() {
                public String mapRow(ResultSet rs, int var2) throws SQLException {
                    return rs.getString(1) + "-"  + rs.getString(2);
                }
            });
        }
    });
    list.forEach(System.out::println);
}

输出的结果是一个String集合,String是id和title通过“-”连接起来的。打印结果如下:

1341734037772668930-title1
1341736555407835137-title2

查询两列,第一列key,第二列value

public <T> List<T> query(String sql, Map<String, ?> params, JdbcTemplateCallback<T> jdbcTemplateCallback)

测试代码

@Test
public void testKeyValue() {
    // language=SQL
    String sql = "SELECT id, title, description, create_time, is_delete, create_by\n" +
            "FROM t_project\n" +
            "WHERE id IN (:id)\n" +
            "  AND title LIKE :title\n" +
            "  AND create_time > :createTime\n" +
            "  AND is_delete = :deleted" +
            "  AND create_by = ${createBy}";

    Map<String, Object> params = new HashMap<>();
    params.put("id", new Long[] { 1341369230607347714L,1341299740150394882L});

    Map<Object, Object> map = gridService.queryForKeyValue(sql, params);
    System.out.println(map);
}

打印结果如下:

{1341299740150394882=this is project title, 1341369230607347714=hello world}

查询单个对象Map,用Optional包装

public Optional<Map<String, Object>> queryForObject(String sql, Map<String, ?> params)

测试代码

@Test
public void testQueryObject() {
    // language=SQL
    String sql = "SELECT id, title, description, create_time, is_delete, create_by\n" +
            "FROM t_project\n" +
            "WHERE id IN (:id)\n" +
            "  AND title LIKE :title\n" +
            "  AND create_time > :createTime\n" +
            "  AND is_delete = :deleted" +
            "  AND create_by = ${createBy}";

    Map<String, Object> params = new HashMap<>();
    params.put("id", new Long[] { 1341369230607347714L});

    Optional<Map<String, Object>> optionalMap = gridService.queryForObject(sql, params);
    System.out.println(optionalMap.isPresent());
    if (optionalMap.isPresent()) {
        System.out.println(optionalMap.get());
    }
}

打印结果如下:

{id=1341369230607347714, title=hello world, description=hello你税的朋友, create_time=2020-12-22 21:05:21.0, is_delete=false, create_by=156629745675451}

NOTE: 如果查询结果超过1条记录,抛出异常。

查询单个Bean对象,用Optional包装

public Optional<Map<String, Object>> queryForObject(String sql, Map<String, ?> params)

测试代码

@Test
public void testQueryBean() {
    // language=SQL
    String sql = "SELECT id, title, description, create_time, is_delete, create_by\n" +
            "FROM t_project\n" +
            "WHERE id IN (:id)\n" +
            "  AND title LIKE :title\n" +
            "  AND create_time > :createTime\n" +
            "  AND is_delete = :deleted" +
            "  AND create_by = ${createBy}";

    Map<String, Object> params = new HashMap<>();
    params.put("id", new Long[] { 1341369230607347714L});

    Optional<Project> optionalMap = gridService.queryForObject(sql, params, Project.class);
    System.out.println(optionalMap.isPresent());
    if (optionalMap.isPresent()) {
        System.out.println(optionalMap.get());
    }
}

打印结果如下:

Project(id=1341369230607347714, title=hello world, description=hello你税的朋友, coverUrl=null, ownerId=null, deleted=null, createdAt=null, createdBy=null, updatedAt=null, updatedBy=null)

重载方法,添加了一个class参数。

NOTE: 如果查询结果超过1条记录,抛出异常。

更新操作

public int update(String sql, Map<String, ?> params)

测试代码

@Test
@Transactional(rollbackFor = Exception.class)
public void testUpdate() {
    // language=SQL
    String sql = "UPDATE t_project SET title = :title, description = :description, is_delete = :deleted\n" +
            "WHERE id IN (:id)\n" +
            "  AND create_time > :createTime\n" +
            "AND create_by = ${createBy}";

    Map<String, Object> params = new HashMap<>();
    params.put("id", new Long[] { 1341369230607347714L, 2L});
    params.put("createTime", "2019-10-22 17:27:41");


//        params.put("title", "new title xx");
    params.put("description", "new description3");
    params.put("deleted", true);

    int count = gridService.update(sql, params);
    System.out.println(count);
}

MappedSharpService

结合 Mybatis 动态SQL的特点,将SQL写在xml文件中。
pom.xml 添加依赖

<dependency>
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis</artifactId>
    <version>3.5.6</version>
    <scope>provided</scope>
</dependency>

获取自定义的集合

public <T> T handle(String selectId, Map<String, Object> params, SharpServiceHandler<T> sharpServiceHandler)
public <T> T handle(SharpService sharpService, String sql, Map<String, Object> params)

测试代码

@Autowired
private MappedSharpService mappedSharpService;

@Test
public void testMappedSharpService() {
    // findById标签必须是<select,不能是<sql>
    Map<String, Object> params = Maps.newHashMapWithExpectedSize(2);
    params.put("title", "");
    params.put("id", 5);

    Optional<Project> optionalProject = mappedSharpService.handle("com.yodean.component.project.modules.project.dao.mapper.ProjectMapper.findById",
            params,
            (sharpService, sql, params1) -> sharpService.queryForObject(sql, params1, Project.class)
    );

    List<Project> projectList = mappedSharpService.handle("com.yodean.component.project.modules.project.dao.mapper.ProjectMapper.findById",
            params,
            (sharpService, sql, params1) -> sharpService.query(sql, params1, Project.class)
    );

    List<Map<String, Object>> projectMapList = mappedSharpService.handle("com.yodean.component.project.modules.project.dao.mapper.ProjectMapper.findById",
            params,
            (sharpService, sql, params1) -> sharpService.query(sql, params1)
    );

    log.info("project is {}", optionalProject.orElse(null));
    log.info("project list as object is {}", projectList);
    log.info("project list as as Map is {}", projectMapList);

}

mapper.xml

<mapper namespace="com.yodean.component.project.modules.project.dao.mapper.ProjectMapper">
    <select id="findById">
         <!-- gridService不能用这种变量形式 id = #{id} 或 id = ?,只能用id = :id -->
        select
        <include refid="column" />
        from t_project where
                id=:id
        <if test="title != null and title !=''">
            and title like ${title}
        </if>
        <if test="description != null and description !=''">
            and description like :description
        </if>
    </select>

    <sql id="column">
        id, title, description, 'haha' as plain
    </sql>
</mapper>

真正执行查询操作的还是 SharpService,只是利用了 Mybatis 动态获取了SQL。
xml中SQL写法有两点需要注意:

  • 标签必须是 select,不能是sql
  • gridService不能用这种变量形式 id = #{id} 或 id = ?,只能用id = :id

GridService/GridUtils

GridService 继承自 SharpService,增加了分页相关的操作。GridUtils 又依赖了 GridService 提供了更方便的操作。所以我们分页操作只需要使用 GridUtils就可以了。

统计结果是数字的单行记录

public static final List<BigDecimal> numericObject(String sql, Map<String, ?> params)

比如:

  • 合计summary: select sum(score), sum(money) from t_xx
  • 平均值avg: select avg(score), avg(money) from t_xx

测试代码

/**
 * 列求和
 */
@Test
public void testSummary() {
    List<BigDecimal> list = GridUtils.numericObject("SELECT SUM(work_time) FROM t_demo", null);
    list.forEach(System.out::println);
}

注意:

  • 记录数只能是一行
  • 数字列,可以列可以映射成 BigDecimal

查询table结果集

public static final Grid<Map<String, Object>> list(String sql, Map<String, ?> params, String countSQL, String... sortableColumns)

比如:

  • countSQL:可以单独指定count的SQL,可以为null
  • sortableColumns:可以允许排序的列,查询字段必须包含id列,不然mysql排序会有问题

测试代码

@Test
public void testList() {
    Map params = new HashMap<>();
    // 通用参数
    params.put("page", 1);
    params.put("size", 2);
    params.put("sidx", "title");
    params.put("sord", "desc");

    // 业务参数
    params.put("title", "version");

//        Grid list = GridUtils.list("SELECT id, title FROM t_demo WHERE title like :title", params, null, new String[]{"title"});
    Grid list = GridUtils.list("SELECT id, title FROM t_demo WHERE title like :title", params, null, "title");
    list.getRows().forEach(System.out::println);
}

SQL打印

    SELECT *
    FROM (SELECT id, title FROM t_demo WHERE UPPER(title) LIKE CONCAT('%', UPPER(:title), '%') ESCAPE '\\') temp_
    ORDER BY temp_.title DESC, temp_.id DESC
    LIMIT 0,2

结果集 Grid 数据结构

public class Grid<T> implements Serializable {

    /**
     * 当前页面索引
     */
    private int page;

    /**
     * 一页显示记录条数
     */
    private int pageSize;

    /***
     * 总纪录数
     */
    private long records;

    /***
     * 总页数
     */
    private long totalPages;

    /***
     * 数据项
     */
    private List<T> rows;

    public static <T> Grid<T> emptyInstance(int pageSize) {
        Grid<T> grid = Grid.builder().totalPages(0)
                .page(1)
                .rows(Collections.EMPTY_LIST)
                .pageSize(pageSize)
                .records(0)
                .build();
        return grid;
    }
}

SQLUtils处理一些SQL层面的操作

查询根据多个id删除记录

public static int deleteByIn(String tableName, String deleteColumn, Collection<?> deleteValues)

测试代码

@Test
public void testDeleteByIn() {
    int deletedCount = SQLUtils.deleteByIn("t_demo", "id", Arrays.asList(1358619329104297985L, 1358619736027283457L));
    System.out.println(deletedCount);
}

查询根据多个id删除记录

public static int deleteByNotIn(String tableName, String deleteColumn, Collection<?> deleteValues)

测试代码和上面类似,略。

查询根据参数个数,生成in的参数?

public static String formatInSQLPlaceHolder(int paramSize)

if paramSize = 4 format as (?, ?, ?, ?)

导出Excel

pom.xml 添加依赖

<dependency>
    <groupId>com.rick.office</groupId>
    <artifactId>sharp-excel</artifactId>
    <version>1.0-SNAPSHOT</version>
    <scope>compile</scope>
</dependency>

ExportUtils 依赖了 GridService 提供了方便的导出操作。只需要根据查询SQL,直接导出excel文件。

public static final void export(String sql, Map<String, ?> params, OutputStream outputStream, List<MapTableColumn> columnList)

测试代码

/**
 * 查询sql导出
 */
@Test
public void testSqlExport() throws IOException {
    Map params = new HashMap<>();
    // 业务参数
    params.put("title", "version");

    ExportUtils.export("SELECT id, title FROM t_demo WHERE title like :title", params,
            new FileOutputStream("/Users/rick/Downloads/export/t_demo.xlsx"),
            Lists.newArrayList(
                    new MapTableColumn("title", "标题") // 导出文件中第一列为标题,且仅导出标题,忽略id
//                        new MapTableColumn("id", "ID")
            ));
}

Controller

编写一个通用的Controller,统一获取Table结果集 Grid ,需要结合之前介绍用到的 MappedSharpServiceGridUtils

QueryController.java

@RestController
@RequestMapping("query")
@RequiredArgsConstructor
public class QueryController {

    private final CommonTableGridBySelectIdService selectIdService;

    private static final Map<String, String> selectIdMap = new HashMap<>();

    static {
        // 在此注册selectId
        selectIdMap.put("product-list", "com.yodean.component.project.modules.project.dao.mapper.ProjectMapper.findById");
    }

    @GetMapping("{selectKey}")
    public Grid request(@PathVariable String selectKey, HttpServletRequest request) {
        return selectIdService.list(selectIdMap.get(selectKey), request);
    }
}

mapper.xml中的查询SQL,之前已经贴过了。

CommonTableGridBySelectIdService.java

@Service
@RequiredArgsConstructor
public class CommonTableGridBySelectIdService {

    private final MappedSharpService mappedSharpService;

    public Grid<Map<String, Object>> list(String selectId, HttpServletRequest request) {
        Assert.notNull(selectId, "selectId cannot be null");
        Map requestParams = HttpServletRequestUtils.getParameterMap(request);
        Object sidx = requestParams.get(PageModel.PARAM_SIDX);

        return  mappedSharpService.handle(selectId, requestParams,
                (SharpServiceHandler<Grid<Map<String, Object>>>) (gridService, sql, params) -> GridUtils.list(sql, params, null
                        , Objects.isNull(sidx) ? null : sidx.toString()));
    }
}

测试:

GET /query/product-list?title='hello world'&amp; description=hello&amp; sidx=title HTTP/1.1
Host: localhost:8912
cache-control: no-cache
Postman-Token: c3707206-0c4f-492e-aee2-586ac3750411

结果集JSON

{
    "success": true,
    "code": 200,
    "msg": "成功",
    "data": {
        "page": 1,
        "pageSize": 15,
        "records": 1,
        "totalPages": 1,
        "rows": [
            {
                "id": 1341368363682439169,
                "title": "hello world",
                "description": "hello",
                "plain": "haha"
            }
        ]
    }
}

应用架构(MVC + 分层架构 + 领域驱动COLA)

应用架构

使用阿里云的应用生成器:https://start.aliyun.com/bootstrap.html 生成应用架构

MVC

MVC模式(Model–view–controller)是软件工程中的一种软件架构模式,把软件系统分为三个基本部分:模型(Model)、视图(View)和控制器(Controller)。

MVC模式最早由Trygve Reenskaug在1978年提出[1],是施乐帕罗奥多研究中心(Xerox PARC)在20世纪80年代为程序语言Smalltalk发明的一种软件架构。MVC模式的目的是实现一种动态的程序设计,使后续对程序的修改和扩展简化,并且使程序某一部分的重复利用成为可能。除此之外,此模式透过对复杂度的简化,使程序结构更加直观。软件系统透过对自身基本部分分离的同时也赋予了各个基本部分应有的功能。专业人员可以依据自身的专长分组:

  • 模型(Model) – 程序员编写程序应有的功能(实现算法等等)、数据库专家进行数据管理和数据库设计(可以实现具体的功能)。
  • 视图(View) – 界面设计人员进行图形界面设计。
  • 控制器(Controller)- 负责转发请求,对请求进行处理。
    https://zh.wikipedia.org/wiki/MVC

架构项目依赖

http://xhope.top/wp-content/uploads/2021/09/a1.png

架构代码分析

├── pom.xml
├── project-model
│   ├── pom.xml
│   └── src
│       └── main
│           ├── java
│           │   └── com
│           │       └── yodean
│           │           └── project
│           │               ├── demos
│           │               │   └── web
│           │               │       └── User.java
│           │               └── mybatis
│           │                   ├── config
│           │                   │   ├── MVCMybatisDemoConfig.java
│           │                   │   └── MybatisDemoConfig.java
│           │                   ├── entity
│           │                   │   ├── MVCMybatisDemoUser.java
│           │                   │   └── MybatisDemoUser.java
│           │                   └── mapper
│           │                       ├── MVCMybatisDemoUserMapper.java
│           │                       └── MybatisDemoUserMapper.java
│           └── resources
│               ├── data.sql
│               ├── mappers
│               │   ├── MybatisDemoUserMapper.xml
│               │   └── mybatisdemouser-mapper.xml
│               └── schema.sql
├── project-service
│   ├── pom.xml
│   └── src
│       └── main
│           └── java
│               └── com
│                   └── yodean
│                       └── project
│                           ├── GetUserInfoService.java
│                           └── impl
│                               └── GetUserInfoServiceImpl.java
├── project-web
│   ├── pom.xml
│   └── src
│       └── main
│           ├── java
│           │   └── com
│           │       └── yodean
│           │           └── project
│           │               ├── controller
│           │               │   └── GreetingController.java
│           │               ├── demos
│           │               │   ├── GreetingThymeleafController.java
│           │               │   └── web
│           │               │       ├── BasicController.java
│           │               │       └── PathVariableController.java
│           │               └── mybatis
│           │                   └── controller
│           │                       └── MybatisDemoUserController.java
│           └── resources
│               ├── static
│               │   └── index.html
│               └── templates
│                   ├── greeting.html
│                   └── greeting2.html
└── start
    ├── pom.xml
    └── src
        ├── main
        │   ├── java
        │   │   └── com
        │   │       └── yodean
        │   │           └── project
        │   │               └── ProjectApplication.java
        │   └── resources
        │       ├── application.properties
        │       ├── static
        │       └── templates
        └── test
            └── java
                └── com
                    └── yodean
                        └── project
                            └── ProjectApplicationTests.java
  • project-model: 属于Model层,包含了数据库操作(通过Mybatis简化持久层操作)和数据模型。「包mybatis」就是DAO层,有实体、配置、Mapper。User.java 就是领域对象。
  • project-service: 业务的封装,依赖项目 project-model,属于Model层和Contrller层
@Service
public class GetUserInfoServiceImpl implements GetUserInfoService{

    @Autowired
    protected MVCMybatisDemoUserMapper mVCMybatisDemoUserMapper;

    @Override
    public void getUserInfoById(String id, Model model)
    {


        //search by id, get UserInfo
        MVCMybatisDemoUser user = mVCMybatisDemoUserMapper.queryUserInfo(id);
        model.addAttribute("name", user.getId())
                .addAttribute("age", user.getAge())
                .addAttribute("height", user.getHeight())
                .addAttribute("weight", user.getWeight());
    }
}
  • project-web: 类Controller属于controller层。templates下的文件是View层,使用 thymeleaf 模版技术。
@RestController
@RequestMapping("/usermybatis")
public class MybatisDemoUserController {

    @Autowired
    private MybatisDemoUserMapper mybatisDemoUserMapper;

    @Autowired
    private GetUserInfoService getUserInfoService;

    // http://127.0.0.1:8080/usermybatis/findAll
    @RequestMapping("/findAll")
    public List<MybatisDemoUser> findAll(){
        return mybatisDemoUserMapper.findAll();
    }

    @GetMapping("/greeting")
    public String greeting(@RequestParam(name="name", required=false, defaultValue="1") String name, Model model) {
        getUserInfoService.getUserInfoById(name, model);
        //这里返回的数据类型是String,但实际上更多的数据通过本函数中Model model传给了前端。返回值String也会被SpringMVC整合为一个ModelAndView,以供前端使用。(Controller可以返回多种数值,比如void、String、ModelAndView。同学们可以自行搜索学习)
        return "greeting";
    }

}

Controller层可以直接依赖model层,操作数据。

  • start:项目启动配置

MVC架构的特点

数据与视图分离,但是业务代码复用性不高,适合前后端不分离的小项目。

分层架构

架构项目依赖

http://xhope.top/wp-content/uploads/2021/09/a2.png

  • project-dao:数据访问层,与底层 MySQL、Oracle、Hbase、OB 等进行数据交互。对应MVC中的Model层,属于基础设施层。
└── com
    └── yodean
        └── project
            ├── dao
            │   ├── dataobject
            │   │   └── UserDO.java
            │   └── mapper
            │       └── UserMapper.java
            └── mybatis
                ├── config
                │   └── MybatisDemoConfig.java
                ├── entity
                │   └── MybatisDemoUser.java
                └── mapper
                    └── MybatisDemoUserMapper.java

1.包dao:

DO: 此对象与数据库表结构一一对应,通过 DAO 层向上传输数据源对象

Mapper: 仓库持久化

2.包mybatis:

entity: Mybatis配置类

entity: 领域对象,数据库可能是一个join查询

Mapper: 仓库持久化

  • project-manager:通用业务处理层,依赖DAO层。它有如下特征:
    1) 对第三方平台封装的层,预处理返回结果及转化异常信息,适配上层接口。
    2) 对 Service 层通用能力的下沉,如缓存方案、中间件通用处理。
    3) 与 DAO 层交互,对多个 DAO 的组合复用。

  • project-api:业务逻辑服务层接口层。

└── com
    └── yodean
        └── project
            └── api
                ├── UserService.java
                └── model
                    └── UserModel.java

UserService.java

public interface UserService {
    String getUserName(Long id);
    UserModel addUser(UserModel user);
}

UserModel: 属于DTO对象

  • project-service:相对具体的业务逻辑服务实现层。依赖dao层、manager层、api层。
└── com
    └── yodean
        └── project
            └── service
                └── UserServiceImpl.java

UserServiceImpl.java

@Component
public class UserServiceImpl implements UserService {

    @Autowired
    private UserMapper userMapper;
    private static final BeanCopier copier = BeanCopier.create(UserModel.class, UserDO.class, false);

    public String getUserName(Long id) {
        UserDO userDO = userMapper.getById(id);
        return userDO != null ? userDO.getName() : null;
    }

    public UserModel addUser(UserModel user) {
        UserDO userDO = new UserDO();
        copier.copy(user, userDO, null);
        Long id = userMapper.insert(userDO);
        user.setId(id);
        return user;
    }
}

实现了模块api的接口 UserService ,并依赖模块dao做持久化操作 UserMapper

  • project-web:网关适配层,依赖dao层、api层。
└── com
    └── yodean
        └── project
            ├── mybatis
            │   └── controller
            │       └── MybatisDemoUserController.java
            └── web
                └── UserController.java

UserController.java

@Component
@RestController
public class UserController {
    @Autowired
    private UserService userService;
    @RequestMapping("/username")
    public String getUserName(@RequestParam("id") Long id) {
        return userService.getUserName(id);
    }
    @RequestMapping("/add")
    @ResponseBody
    public UserModel addUser(@RequestParam("name") String name, @RequestParam("age") Integer age) {
        UserModel user = new UserModel();
        user.setName(name);
        user.setAge(age);
        return userService.addUser(user);
    }
}

Web层可以直接依赖dao层,操作数据。

  • start: 项目启动配置,项目装配。

分层架构的特点

数据与视图分离,业务代码复用性高,适合前后端分离的项目。

阿里巴巴Java开发推荐分层结构如图所示

https://raw.githubusercontent.com/alibaba/p3c/p3c-pmd-2.1.0/p3c-gitbook/images/alibabaLevel.png

领域驱动COLA架构

DDD以业务为核心,解耦外部依赖,分离业务复杂度和技术复杂度。
DDD的架构模式有六边形架构、洋葱圈架构、整洁架构、COLA架构等。
COLA项目地址:https://github.com/alibaba/COLA
COLA参考资料:COLA 4.0:应用架构的最佳实践

COLA架构图

http://xhope.top/wp-content/uploads/2021/09/a3.png
由图所示,COLA架构采用了4层架构。Adapter层(用户接口层),App 层(应用层),Domain层(领域层),Infrastructure层(基础设施层)。

架构项目依赖

http://xhope.top/wp-content/uploads/2021/09/a4.png

  • project-controller:适配层(Adapter Layer),负责对前端展示(web,wireless,wap)的路由和适配,对于传统B/S系统而言,adapter就相当于MVC中的controller
  • project-app:应用层(Application Layer),主要负责获取输入,组装上下文,参数校验,调用领域层做业务处理,如果需要的话,发送消息通知等。层次是开放的,应用层也可以绕过领域层,直接访问基础实施层;
  • project-client:Client SDK,服务对外透出的API;
  • project-domain:领域层(Domain Layer),主要是封装了核心业务逻辑,并通过领域服务(Domain Service)和领域对象(Domain Entity)的方法对App层提供业务实体和业务逻辑计算。领域是应用的核心,不依赖任何其他层次;
  • project-infrastructure:基础实施层(Infrastructure Layer),主要负责技术细节问题的处理,比如数据库的CRUD、搜索引擎、文件系统、分布式服务的RPC等。此外,领域防腐的重任也落在这里,外部依赖需要通过gateway的转义处理,才能被上面的App层和Domain层使用。

DDD-经典四层架构应用
通过现实例子显示领域驱动设计的威力

源代码下载

architecture.zip

PO BO DTO VO DO的区别

  • DO(Data Object):此对象与数据库表结构一一对应,通过 DAO 层向上传输数据源对象。
  • DTO(Data Transfer Object):数据传输对象,Service 或 Manager 向外传输的对象。
  • BO(Business Object):业务对象,可以由 Service 层输出的封装业务逻辑的对象。
  • Query:数据查询对象,各层接收上层的查询请求。注意超过 2 个参数的查询封装,禁止使用 Map 类 来传输。
  • VO(View Object):显示层对象,通常是 Web 向模板渲染引擎层传输的对象。

更多参考:

看懂UML类图

类之间到关系

  • 泛化关系
  • 实现关系
  • 聚合关系
  • 组合关系
  • 关联关系
  • 依赖关系

泛化关系

继承关系为 is-a的关系;两个对象之间如果可以用 is-a 来表示,就是继承关系:(..是..)

eg:自行车是车、猫是动物

泛化关系用一条带空心箭头的直接表示;如下图表示(A继承自B);

https://design-patterns.readthedocs.io/zh_CN/latest/_images/uml_generalization.jpg

最终代码实现:泛化关系 表现为「继承(extend)」。

public class A {

}

public class B extend A {

}
public interface A {

}

public interface B extend A {

}

实现关系

实现关系用一条带空心箭头的虚线表示;

eg:”车”为一个抽象概念,在现实中并无法直接用来定义对象;只有指明具体的子类(汽车还是自行车),才 可以用来定义对象(”车”这个类在C++中用抽象类表示,在JAVA中有接口这个概念,更容易理解)

https://design-patterns.readthedocs.io/zh_CN/latest/_images/uml_realize.jpg

最终代码实现:实现关系 表现为「实现(implements)」。

public interface A {

}

public class B implements A {

}

聚合关系

聚合关系用一条带空心菱形箭头的直线表示,如下图表示A聚合到B上,或者说B由A组成;

https://design-patterns.readthedocs.io/zh_CN/latest/_images/uml_aggregation.jpg

聚合关系用于表示实体对象之间的关系,表示整体由部分构成的语义;例如一个部门由多个员工组成;

与组合关系不同的是,整体和部分「不是强依赖」的,即使整体不存在了,部分仍然存在;例如, 部门撤销了,人员不会消失,他们依然存在;

最终代码实现:聚合关系 表现为「类属性」。

public class Department {

}

public class User {
    private Department department;
}

组合关系

组合关系用一条带实心菱形箭头直线表示,如下图表示A组成B,或者B由A组成;

https://design-patterns.readthedocs.io/zh_CN/latest/_images/uml_composition.jpg

与聚合关系一样,组合关系同样表示整体由部分构成的语义;比如公司由多个部门组成;

但组合关系是一种「强依赖」的特殊聚合关系,如果整体不存在了,则部分也不存在了;例如, 公司不存在了,部门也将不存在了;

最终代码实现:聚合关系 表现为「类属性」。

public class Address {

}

public class User {
    private Address address;
}

关联关系

关联关系是用一条直线表示的;它描述不同类的对象之间的结构关系;它是一种静态关系, 通常与运行状态无关,一般由常识等因素决定的;它一般用来定义对象之间静态的、天然的结构; 所以,关联关系是一种「强关联」的关系;

比如,乘车人和车票之间就是一种关联关系;学生和学校就是一种关联关系;

关联关系默认不强调方向,表示对象间相互知道;如果特别强调方向,如下图,表示A知道B,但 B不知道A;

关联关系分为:单向关联和双向关联

https://design-patterns.readthedocs.io/zh_CN/latest/_images/uml_association.jpg

最终代码实现:关联关系 表现为「类属性」。

public class School {
    private Set<Student> students;
}

public class Student {
    private School school;
}

依赖关系

依赖关系是用一套带箭头的虚线表示的;如下图表示A依赖于B;他描述一个对象在运行期间会用到另一个对象的关系;

https://design-patterns.readthedocs.io/zh_CN/latest/_images/uml_dependency.jpg

与关联关系不同的是,它是一种临时性的关系,通常在运行期间产生,并且随着运行时的变化; 依赖关系也可能发生变化;

显然,依赖也有方向,双向依赖是一种非常糟糕的结构,我们总是应该保持单向依赖,杜绝双向依赖的产生(Spring注入到时候不要循环依赖);

注:在最终代码中,依赖关系体现为类构造方法及类方法的传入参数,箭头的指向为调用关系;依赖关系除了临时知道对方外,还是“使用”对方的方法和属性;

public class A {

}

public class B {

    public void fun(A a) {

    }
}

以订单为例的综合类图

http://xhope.top/wp-content/uploads/2021/09/22.png

参考:
https://design-patterns.readthedocs.io/zh_CN/latest/
https://www.cnblogs.com/uml-tool/p/8568396.html

jackson注解-JPA-事务处理

Jackson注解

如果是POJO对象,有Getter和Setter方法,才能序列化和反序列化;

  • @JsonInclude

@JsonInclude(JsonInclude.Include.NON_EMPTY),如果字段是空,就忽略显示。如果是数组为空,显示[]。对象为空,显示{}。这是最佳实践

  • @JsonIgnore

忽略该字段,序列化和反序列化都生效

  • 类注解 @JsonIgnoreProperties

@JsonIgnoreProperties(value = {“refreshTime”}, allowGetters = true)。属性refreshTime, 序列化生效,反序列化不生效。可以对多个属性统一处理。

  • @JsonFormat

指定时间的格式,@JsonFormat(timezone = “GMT+8″, pattern = “yyyy/MM/dd HH:mm:ss”)可以使用在LocalDateTime Instant Date对象上。

注意:序列化和反序列化都是这个格式。如果没有指定格式,则为格林尼治格式。最佳实践,为时间类型对属性指定格式。

  • @JsonValue

使用此注解时,序列化当前属性的值,对象(toString),忽略其他属性。

  • @JsonAlias(“myPage”)

反序列化的时候,将JSON属性myPage映射POJO上,序列化的不受影响。

  • @JsonProperty

@JsonProperty(value = “myRow”, access = JsonProperty.Access.READ_WRITE)
反序列的时候,将JSON属性myRow映射POJO上,序列的属性值是myRow。 这个地方是跟@JsonAlias("myRow")的区别

access控制属性的读写,功能类似@JsonIgnorePropertiesallowGetters = true和allowSetters = true方法

  • @JsonView

在controller中使用注解@JsonView(Refresh.addressDetail.class),表示「只」序列化POJO中有@JsonView(Refresh.addressDetail.class)注解的属性。对选择性序列化有用

  • @JsonSerialize(using = AddressJsonSerializer.class)

指定序列化:address对象序列化成字符串

public class AddressJsonSerializer extends JsonSerializer<Address> {

    @Override
    public void serialize(Address address, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
        jsonGenerator.writeString(address.toString());
    }
}
  • @JsonDeserialize(using = PhoneJsonDeserializer.class)

指定反序列化:字符串0523-09024434反序列化成phone对象

public class PhoneJsonDeserializer extends JsonDeserializer<Phone> {
    @Override
    public Phone deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, JsonProcessingException {
        JsonNode node = jsonParser.getCodec().readTree(jsonParser);
        String[] phonetArr = node.asText().split("-");
        Phone phone = new Phone();
        phone.setCode(phonetArr[0]);
        phone.setNumber(phonetArr[1]);
        return phone;
    }
}
  • @JsonComponent

如果使用Jackson对JSON数据进行序列化和反序列化,则可以编写自己的JsonSerializer和JsonDeserializer。然后通过@JsonSerialize和@JsonDeSerialize来指定具体类是否使用。

同时,SpringBoot提供了一个可选的@JsonComponent注释,可以将对应的JsonSerializer和JsonDeserializer直接注入为Spring Beans,从而实现全局化处理。

  • @JsonCreator
    @JsonCreator
    private Refresh(@JsonProperty("id") Long id) {
        this.id = id;
    }

当json在反序列化时,默认选择类的无参构造函数创建类对象,当没有无参构造函数时会报错,@JsonCreator作用就是指定反序列化时用的无参构造函数。构造方法的参数前面需要加上@JsonProperty,否则会报错。

  • @JsonManagedReference、@JsonBackReference

jackson中的@JsonBackReference和@JsonManagedReference,以及@JsonIgnore均是为了解决对象中存在双向引用导致的无限递归(infinite recursion)问题。这些标注均可用在属性或对应的get、set方法中。

@JsonBackReference和@JsonManagedReference:这两个标注通常配对使用,通常用在父子关系中。@JsonBackReference标注的属性在序列化(serialization,即将对象转换为json数据)时,会被忽略(即结果中的json数据不包含该属性的内容)。@JsonManagedReference标注的属性则会被序列化。在序列化时,@JsonBackReference的作用相当于@JsonIgnore,此时可以没有@JsonManagedReference。但在反序列化(deserialization,即json数据转换为对象)时,如果没有@JsonManagedReference,则不会自动注入@JsonBackReference标注的属性(被忽略的父或子);如果有@JsonManagedReference,则会自动注入自动注入@JsonBackReference标注的属性。

  • 代码片段
@Entity
@Table(name = "t_refresh")
@Data
@JsonInclude(JsonInclude.Include.NON_EMPTY)
@JsonIgnoreProperties(value = {"refreshTime"}, allowGetters = true, allowSetters = true)
public class Refresh implements Serializable {

    @JsonCreator
    private Refresh(@JsonProperty("id") Long id) {
        this.id = id;
    }

    @Id
    @GeneratedValue(strategy= GenerationType.IDENTITY)
    // @JsonValue
    private Long id;

    @JsonIgnore
    private String city;

    /**
     * 刷新,侦测时间点
     * mysql: datetime类型
     */
    @JsonFormat(timezone = "GMT+8", pattern = "yyyy/MM/dd HH:mm:ss")
    /**
     * 默认就使用这个
     */
    @JsonSerialize(using = LocalDateTimeSerializer.class)
    private LocalDateTime refreshTime;

    /**
     * @Temporal 持久化注解
     * mysql: 默认time类型
     */
    @JsonFormat(timezone = "GMT+8", pattern = "HH:mm:ss")
    private Date createDate;

    /**
     * @Temporal 持久化注解
     * mysql: 指定类型为TIMESTAMP
     * Date +  @Temporal == LocalDateTime
     */
    @JsonFormat(timezone = "GMT+8", pattern = "yyyy/MM/dd HH:mm:ss")
    @Temporal(TemporalType.TIMESTAMP)
    private Date updateDate;

    @JsonFormat(timezone = "GMT+8", pattern = "yyyy/MM/dd")
    @Temporal(TemporalType.DATE)
    private Date hireDate;

    /**
     * 页码
     */
    @JsonAlias("myPage")
    private Integer page;

    /**
     * 第几行
     */
    @JsonProperty(value = "myRow", access = JsonProperty.Access.READ_ONLY)
    private Integer row;

    /**
     * 总排名(含广告)
     */
    private Integer score;

    private Boolean isMatched;

    private String path;


    public interface addressDetail{}
    public interface phoneDetail{}


    @Transient
    @JsonSerialize(using = AddressJsonSerializer.class)
    @JsonView(addressDetail.class)
    private Address address;

    @Transient
    @JsonDeserialize(using = PhoneJsonDeserializer.class)
    @JsonView(phoneDetail.class)
    private Phone phone;

    public void setScore(Integer score) {
        System.out.println(score);
        this.score = score;
    }
}

JPA注解

  • Transient

默认使用字段映射, 通过@Transient设置,不用数据库字段的映射。

  • @Temporal(TemporalType.TIME)

注意:@Temporal should only be set on a java.util.Date or java.util.Calendar property,不能用在LocalDateTime上,
Date + @Temporal(TemporalType.TIMESTAMP) == LocalDateTime。最佳实践,优先使用LocalDateTime

    {
        "refreshTime": "2020-11-17T09:59:27",  // LocalDateTime
        "updateDate": "2020-11-17T15:59:27.000+0000", // Date +  @Temporal(TemporalType.TIMESTAMP)
        "createDate": "1970-01-02T00:30:18.000+0000", // Date,默认TemporalType.TIME
        "hireDate": "2020-11-17", // Date + TemporalType.DATE
    }

  • @MappedSuperclass

基于代码复用和模型分离的思想,在项目开发中使用JPA的@MappedSuperclass注解将实体类的多个属性分别封装到不同的非实体类中。例如,数据库表中都需要id来表示编号,id是这些映射实体类的通用的属性,交给jpa统一生成主键id编号,那么使用一个父类来封装这些通用属性,并用@MappedSuperclas标识。

注意:

1.标注为@MappedSuperclass的类将不是一个完整的实体类,他将不会映射到数据库表,但是他的属性都将映射到其子类的数据库字段中。

2.标注为@MappedSuperclass的类不能再标注@Entity或@Table注解,也无需实现序列化接口。

  • @PreUpdate、@PrePersist、@PreRemove

用于为相应的生命周期事件指定回调方法。

  • @Enumerated

使用此注解映射枚举字段,以String类型存入数据库

注入数据库的类型有两种:EnumType.ORDINAL(Interger)、EnumType.STRING(String)

  • @Embedded、@Embeddable

当一个实体类要在多个不同的实体类中进行使用,而其不需要生成数据库表。比如:address和user。只想生成一张表user,但是是两个类

  1. @Embeddable:注解在类上,表示此类是可以被其他类嵌套
  2. @Embedded:注解在属性上,表示嵌套被@Embeddable注解的同类型类
  • @CreatedDate、@CreatedBy、@LastModifiedDate、@LastModifiedBy

表示字段为创建时间字段(insert自动设置)、创建用户字段(insert自动设置)、最后修改时间字段(update自定设置)、最后修改用户字段(update自定设置)

  用法:

    1、@EntityListeners(AuditingEntityListener.class):申明实体类并加注解

    2、@EnableJpaAuditing:在启动类中加此注解

    3、在实体类中属性中加上面四种注解

    4、自定义添加用户

import org.springframework.context.annotation.Configuration;
import org.springframework.data.domain.AuditorAware;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
@Configuration
public class UserIDAuditorBean implements AuditorAware<Long> {
    @Override
    public Long getCurrentAuditor() {
        SecurityContext ctx = SecurityContextHolder.getContext();
        if (ctx == null) {
            return null;
        }
        if (ctx.getAuthentication() == null) {
            return null;
        }
        if (ctx.getAuthentication().getPrincipal() == null) {
            return null;
        }
        Object principal = ctx.getAuthentication().getPrincipal();
        if (principal.getClass().isAssignableFrom(Long.class)) {
            return (Long) principal;
        } else {
            return null;
        }
    }
}
  • @DynamicInsert

@DynamicInsert属性:设置为true,表示insert对象的时候,生成动态的insert语句,如果这个字段的值是null就不会加入到insert语句中,默认false。比如希望数据库插入日期或时间戳字段时,在对象字段为空定的情况下,表字段能自动填写当前的sysdate

  • @DynamicUpdate

@DynamicUpdate属性:设置为true,表示update对象的时候,生成动态的update语句,如果这个字段的值是null就不会被加入到update语句中,默认false。
比如只想更新某个属性,但是却把整个属性都更改了,这并不是我们希望的结果,我们希望的结果是:我更改了哪写字段,只要更新我修改的字段就够了

  • AttributeConverter

实体属性类型转换器。更多信息参考https://xhope.top/?p=1122

  • 小技巧

一对多,由多的一方维护关联关系时,如公司group和部门departments。前端传递JSON,可以这么传:

{
    "id": 5
    "name": 'Google',
    "departmentIds": [1, 2]
}

后端:

    @Transient
    private Long[] departmentIds;

    public void setDepartmentIds(Long[] departmentIds) {
        this.departmentIds = departmentIds;

        // 反序列化的时候,进行处理
        if (Objects.nonNull(departmentIds) && departmentIds.length > 0) {
            departments = new ArrayList<>(departmentIds.length);
            for (Long departmentId : departmentIds) {
                departments.add(new Department(departmentId));
            }
        }
    }

JPA关联关系

一对多@OneToMany

@Target({ElementType.METHOD, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface OneToMany {
    Class targetEntity() default void.class;

    CascadeType[] cascade() default {};

    FetchType fetch() default FetchType.LAZY;

    String mappedBy() default "";

    boolean orphanRemoval() default false;
}

用在集合属性上:如List表示有序,Set表示不重复

Group.java

    @OneToMany(cascade = CascadeType.PERSIST, orphanRemoval= true, mappedBy = "group")
    private List<Department> departments;

“一对多”或“多对一”,需要在多的一方加外键。通过注解@JoinColumn完成

    @JoinColumn(name = "group_id")

这个既可以加在“多”的一方,也可以加在“一”的一方。
1. 加在“一”的一方表示由“一”的一方维护。
2. 加在“多”的一方表示由多的一方维护。“一”多一方必须使用mappedBy交出控制。否则可能会创建表(字段)

mappedBy:

  1. 只有OneToOne,OneToMany,ManyToMany上才有mappedBy属性,ManyToOne不存在该属性;
  2. mappedBy标签一定是定义在被拥有方的,他指向拥有方;
  3. mappedBy的含义,应该理解为,拥有方能够自动维护跟被拥有方的关系,当然,如果从被拥有方,通过手工强行来维护拥有方的关系也是可以做到的;
  4. mappedBy跟joinColumn/JoinTable总是处于互斥的一方,可以理解为正是由于拥有方的关联被拥有方的字段存在,拥有方才拥有了被拥有方。mappedBy这方定义JoinColumn/JoinTable总是失效的,不会建立对应的字段或者表。

mappedBy表示声明自己不是一对多的关系维护端,由对方来维护,是在一的一方进行声明的。mappedBy的值应该为一的一方的表名。

orphanRemoval:

  1. jpa 中 orphanRemoval 属性,如果为 true 的话,想要删掉子集合数据,那么调用子集合list 的 clear 方法清空,并且断关系可以直接在数据库中删除子集合数据, 不能直接设置 为null,否则抛出异常.
  2. 如果没有该属性,调用子集合list 的 clear 方法清空,并且断关系则在数据库中把 子表数据中保存的主表id 设置为空,断开关系;
  3. 而cascade 是总开关,如果 这里没有设置 CascadeType.all 或者 delete ,那么就算 orphanRemoveal 设置为 true 也无法执行删除.

FetchType:

FetchType.LAZY:懒加载,加载一个实体时,定义懒加载的属性不会马上从数据库中加载

FetchType.EAGER:急加载,加载一个实体时,定义急加载的属性会立即从数据库中加载

CascadeType

CascadeType.MERGE级联更新:若items属性修改了那么order对象保存时同时修改items里的对象。对应EntityManager的merge方法 (较常用 )

CascadeType.PERSIST级联保存:对order对象保存时也对items里的对象也会保存。对应EntityManager的presist方法

CascadeType.REFRESH级联刷新:获取order对象里也同时也重新获取最新的items时的对象。对应EntityManager的refresh(object)方法有效。即会重新查询数据库里的最新数据

CascadeType.REMOVE级联删除:对order对象删除也对items里的对象也会删除。对应EntityManager的remove方法

CascadeType.ALL包含所有;

多对一@ManyToOne

public @interface ManyToOne {
    Class targetEntity() default void.class;

    CascadeType[] cascade() default {};

    FetchType fetch() default FetchType.EAGER;

    boolean optional() default true;
}

optional
同表示是否允许为空。比如公司和部门。不允许没有公司的部门,所以需要设置optional = false

    Group group = new Group();
    group.setId(1L);

    Department department = new Department();
    department.setName("111");
    department.setGroup(group); //  @ManyToOne(optional = false) 必须设置group
    departmentRepository.save(department);

一对一

多对多@ManyToMany

“多对多”通过JoinTable增加一张中间表,跟@JoinColumn增加一个外键字段的作用一致的,提供“连接”。

public @interface ManyToMany {
    Class targetEntity() default void.class;

    CascadeType[] cascade() default {};

    FetchType fetch() default FetchType.LAZY;

    String mappedBy() default "";
}
    @ManyToMany(fetch = FetchType.EAGER)
    @JoinTable(name = "t_user_role", 
    joinColumns = { @JoinColumn(name = "user_id") },
    inverseJoinColumns = { @JoinColumn(name = "role_id") })
    public Set<Role> getRoles() {
        return roles;
    }

这样就会创建一张中间表t_user_role,分别有两个字段user_idrole_id,分别指向user和role表。

如果想在中间表中,添加自己的「id」, 「创建时间」等。@ManyToMany就不能实现了。

可以自己通过创建中间表UserRole

User 1 <-> N UserRole
Role 1 <-> N UserRole

User.java

@Data
@Entity
@Table(name = "t_user")
public class User implements Serializable {

    @Id
    @GeneratedValue(strategy= GenerationType.IDENTITY)
    private Long id;

    private String name;

    @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
    @JoinColumn(name = "user_id")
    private List<UserRole> roleList;
}

Role.java

@Data
@Entity
@Table(name = "t_role")
public class Role implements Serializable {

    @Id
    @GeneratedValue(strategy= GenerationType.IDENTITY)
    private Long id;

    private String name;
}

UserRole.java

@Data
@Entity
@Table(name = "t_user_role")
public class UserRole {

    @Id
    @GeneratedValue(strategy= GenerationType.IDENTITY)
    private Long id;

    private LocalDateTime createdDate;

    @ManyToOne
    private User user;

    @ManyToOne
    @JoinColumn(name = "role_id")
    private Role role;

}

Test.java

    Role role = new Role();
    role.setName("admin1");
    roleRepository.save(role); // 保存角色

    User user = new User();
    user.setName("Rick1");


    UserRole userRole = new UserRole();
    userRole.setCreatedDate(LocalDateTime.now());
    userRole.setRole(role); // 绑定角色关联关系

    user.setRoleList(Lists.newArrayList(userRole)); // 绑定用户跟关系表
    userRole.setRole(role);

    userRepository.save(user);

事务处理

通过spring注解@Transactional实现。 org.springframework.transaction.annotation.Transactional,这个注解对JPA操作和JdbcTemplate,都能统一管理事务,也就是说可以在一个service中同时使用JdbcTemplate操作,又可以使用JPA操作。

    @Test
    @Transactional
    public void testAddTransactional() {
        Department department = new Department();
        department.setName("IT");
        departmentRepository.save(department);

        int a = 5/0; // 抛出异常并回滚

        Role role = new Role();
        role.setName("admin");
        roleRepository.save(role);

    }
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Transactional {
    @AliasFor("transactionManager")
    String value() default "";

    @AliasFor("value")
    String transactionManager() default "";

    Propagation propagation() default Propagation.REQUIRED;

    Isolation isolation() default Isolation.DEFAULT;

    int timeout() default -1;

    boolean readOnly() default false;

    Class<? extends Throwable>[] rollbackFor() default {};

    String[] rollbackForClassName() default {};

    Class<? extends Throwable>[] noRollbackFor() default {};

    String[] noRollbackForClassName() default {};
}
  • 0、如果调用方法加@Transactional,嵌套调用的方法,发生Runtime的异常,也会回滚,即使嵌套调用的方法没加@Transactional;反之,嵌套调用的方法加注解@Transactional,发生异常,但调用方法没有加注解,那么,不回滚。??TODO,需要在深入理解下“spring的事务传播行为”,如果嵌套但方法是当前对象的方法,this是当前被代理的对象,已经不是spring自动代理的对象了。之前做cache的时候,也遇到类似的情况。Spring事务管理
  • 1、异常在A方法内抛出,则A方法就得加注解
  • 2、多个方法嵌套调用,如果都有@Transactional 注解,则产生事务传递,默认 Propagation.REQUIRED
  • 3、如果注解上只写 @Transactional 默认只对 RuntimeException 回滚,而非 Exception 进行回滚
  • 如果要对 checked Exceptions 进行回滚,则需要 @Transactional(rollbackFor = Exception.class)

更多参考
* https://www.cnblogs.com/taven/p/5942384.html
* https://www.cnblogs.com/flydean/p/12680284.html

Vue生命周期执行测试

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Document</title>
</head>
<body>
    <div id="root">
        <div>{{fullName}}</div>
        <button @click="change">change firstName</button>
    </div>


    <script src="https://cdn.bootcdn.net/ajax/libs/vue/2.6.9/vue.min.js"></script>
    <script>
        new Vue({
            el: '#root',
            data: {
                firstName: 'Rick',
                lastName: 'Xu'
            },
            computed: {
                /*
                    使用的时候才会执行: 1. 页面绑定, 2 代码调用
                    不要再computed,对data属性的值进行修改
                */
                fullName: function() {
                    let fullName = this.firstName + ' ' + this.lastName
                    console.log('-  computed ', fullName)
                    return fullName
                }
            },
            watch: {
                /*
                    整个生命周期,只会出发一次监控
                */
                firstName: function(val, old) {
                    console.log('-  watch ', old, ' => ', val)
                }
            },
            created() {
                /*
                    // 还未完成页面渲染
                    ajax请求放在created中,比在mounted好

                    1. 会调用computed
                    2. 因为mounted也对「firstName」进行了修改,「watch」mounted后被调用一次。created不调用
                */

                // console.log(this.fullName) // Rick.Xu 手动调用
                this.firstName = 'Jim'
                console.log('1. created')
                // 1. 页面绑定了computed属性,会调用一次

            },
            mounted() {
                /*
                    完成页面渲染
                    会调用watch和computed方法
                    操作dom,jquery事件绑定在这里处理
                */
                this.firstName = 'Ashley'
                console.log('2. mounted')
            },
            updated() {
                console.log('3. updated')
            },
            methods: {
                change: function() {
                    /*
                        先执行watch,再computed
                    */
                    this.firstName = 'Lucy'
                    console.log('4. change')
                }
            }
        })

    </script>
</body>
</html>

http://xhope.top/wp-content/uploads/2020/11/qq.png