前言
8、特殊 SQL 的执行
8.1 模糊查询
mapper 接口中方法的定义:
mapper 映射文件中 sql 语句的编写,三种方法都可以实现:
注意:
- 这里不可以使用
select * from t_user where username like '%#{username}%'
,即将#{}
这种占位符赋值获取参数值的放在 单引号 里面; - 这样直接将单引号里面的三个字符都当作字符串的一部分,而不会解析成占位符,看报错信息可以看出来,?直接放在单引号内部,当成一个字符了,而不会被当成一个占位符。
测试类的编写:
//测试模糊查询
@Test
public void testGetUserByLike(){
SqlSession sqlSession = SqlSessionUtils.getSqlSession();
SQLMapper mapper = sqlSession.getMapper(SQLMapper.class);
List<User> list = mapper.getUserByLike("王");
System.out.println(list);
}
结果:
8.2 批量删除
mapper 接口中方法的定义:
mapper 映射文件中 sql 语句的编写,这里只能用 ${}
这种字符串拼接的方法来实现:
注意:
- 这里不可以使用
delete from t_user where id in (#{ids})
; #{}
这种占位符赋值的方式,会自动给参数加上单引号,从报错信息可以看出来,我想要一个 Double 类型的数据,你给我一个 ‘1,2,4’ 的字符串:
测试类的编写:
//测试批量删除
@Test
public void testDeleteMore(){
SqlSession sqlSession = SqlSessionUtils.getSqlSession();
SQLMapper mapper = sqlSession.getMapper(SQLMapper.class);
int result = mapper.deleteMore("1,2,4");
System.out.println(result);
}
结果:
8.3 动态设置表名
mapper 接口和 mapper 映射文件的实现:
注意:
- 和上面情况类似,由于
#{}
占位符赋值的方法在传入参数的时候会自动为参数加上单引号,所以这里不能用这种方法来实现设置表名的方法,因为 sql 语句不识别带单引号的表名
Cause: java.sql.SQLSyntaxErrorException: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near ''t_user'' at line 1
8.4 添加功能获取自增的主键
比如我们有下面的两个表,在新建一个班级信息的时候,我们需要获取该班级添加到数据库之后的自增主键,才能在该班级相对应的学生表中设置班级 Id 这一项。
-
JDBC 实现的时候有一个方法
ResultSet resultSet = ps.getGeneratedKeys();
,可以获取自增的主键,那么 MyBatis 如何实现呢? -
MyBatis 的实现,通过在 mapper 映射文件中设置
useGeneratedKeys="true"
属性:
mapper 接口中方法的定义:
mapper 映射文件中 sql 语句的编写:
解释:
useGeneratedKeys
表示返回集我们要用到自增的主键这个值;keyProperty
表示我们将自增的主键赋给传回的结果集中的哪一个属性上,主键值不能作为方法的返回值(因为增删改的返回值是固定的,就是受影响的行数),只能将主键放在我们传回来的参数的某一个属性中,所以这里只能将自增 id 放在用户的 id 属性中传回来。
测试类的编写:
//测试插入一条数据,是否能获取到数据库为它自动生成的主键
@Test
public void testInsertUser(){
SqlSession sqlSession = SqlSessionUtils.getSqlSession();
SQLMapper mapper = sqlSession.getMapper(SQLMapper.class);
User user = new User(null, "张三", "123", 23, "男", "zhangsan@163.com");
mapper.insertUser(user);
System.out.println(user);
}
结果,我们插入了一个 null 值,但是返回的对象中有主键 id =13:
9、自定义映射 resultMap
9.1 resultMap 处理字段和属性的映射关系(即数据库中的字段名和类中的属性名称不一致的情况)
-
在 mapper 映射文件中,查询 sql 语句的时候,为字段起别名,即
select eid,emp_name empName,age,sex,email,did from t_emp
: -
在
mybatis-config.xml
文件中设置全局配置,只能将自定义属性的_自动映射为驼峰
<setting name="mapUnderscoreToCamelCase" value="true"/>
- 通过 resultMap 设置自定义的映射关系
9.2 多对一映射关系
- 创建多对一对应的属性,并且创建 get/set 方法;
- 我们现在查询员工对应的部门名称,可以使用多表联查,但是查出来的是一个 int 类型的 did,对应不上我们自己定义的 dept 类型的 dept 属性。
- 级联属性赋值
- association 方法
注意:
- 使用 association 标签方式进行多表联查的时候,映射文件中 Emp 中的所有字段名都要写,不然当前属性的值为 null ,不会进行获取;
javaType
:和 Emp 这个类中定义的那个 Dept 属性对应,反射获取能和当前想要的属性对应上的实现类,然后将那个类赋值给当前 Emp 的 Dept 属性。
- 分步查询(最常用的,将不同代码拆分开,代码多功能复用)
分步查询的第一步:先根据员工 id 获取员工的信息
分步查询的第二步:通过部门 id 获取部门信息(有没有发现这个方法就算不作为分步查询的第二步,也可以作为根据部门 id 查询部门信息的一个方法)
那么在 Emp 的映射文件中如何将两步查询到的信息关联起来呢,只能根据部门 id :
解释:
- association 标签中的 column 属性就将两个查询到的信息关联了起来;
- select 属性中写的是 mapper 接口的全类名(唯一标识);
分步查询的优点:
- 可以实现延迟加载,但是必须在核心配置文件中设置全局配置信息(配置如下两个信息):没有全局设置的话,默认的是立即加载;
lazyLoadingEnabled
:延迟加载的全局开关。当开启时,所有关联对象都会延迟加载;aggressiveLazyLoading
:当开启时,任何方法的调用都会加载该对象的所有属性。 否则,每个属性会按需加载。
- 在
mybatis-config.xml
中配置<setting name="lazyLoadingEnabled" value="true"/>
,这个时候就相当于开启了延迟加载,此时就可以实现按需加载,获取的数据是什么,就只会执行相应的 sql; - 此时可通过 association 和 collection 中的
fetchType
属性设置当前的分步查询是否使用延迟加载,fetchType=“lazy(延迟加载)|eager(立即加载)”;
可以看出,开启延迟加载之后,不需要的 sql 语句就不会预编译和执行,节省资源。
9.3 一对多映射关系
- 在当前类中创建多对一的属性集合,并且生成对应的 get/set 方法;
- collection:处理一对多的映射关系
-
ofType
:表示该属性所对应的集合中存储数据的类型,就是在 Dept 类中的属性集合的泛型中定义的那个属性。 -
目的:设置字段和属性的映射关系
-
通过 ofType 获取集合里存储的数据的类型,然后根据这个类型获取相应的属性(MyBatis内部操作),映射关系建立后,创建该类型的对象(此处是每一个 emp 对象),最后把每一个对象放在集合中(List)
- 分步查询(优点和上面一样),分步查询只要第一步的时候用 resultMap 就可以了,第二步的时候直接映射为属性的类型即可。
分步查询的第一步:先根据部门 id 获取部门的信息
解释:
column
:分步查询条件。
分步查询的第二步:通过部门 id 去员工表中查询所有的员工信息
10、动态 SQL
- Mybatis 框架的动态 SQL 技术是一种根据特定条件动态拼装 SQL 语句的功能,它存在的意义是为了解决拼接 SQL 语句字符串时的痛点问题;
- 比如我们操作的是员工表,一般情况下,我们用的都是表单项,让用户输入一些员工姓名或者年龄范围或者性别来进行一些查询操作
where ename = '?' and age = ? and sex = "?"
- 如果我们没有这个条件,比如 ename 用户没有输入信息,那么 ename = null 或者 ename = “” ,这个时候 ename 这个条件不应该出现在 sql 语句中
如果我们 ename、age、sex 都没有的话,那么我们连 where 这个也不应该出现在 sql 语句中,这样就涉及到了 sql 语句的动态拼接。
10.1 if - where - trim 标签
1、if:
- 根据标签中test属性所对应的表达式决定标签中的内容是否需要拼接到SQL中。
2、where:
- 当 where 标签中有内容时,会自动生成 where 关键字,并且将内容前多余的 and 或 or 去掉;
- 当 where 标签中没有内容时,此时 where 标签没有任何效果;
- 注意:where 标签不能将其中内容后面多余的 and 或 or 去掉。
3、trim:
- 若标签中有内容时:
prefix|suffix
:将 trim 标签中内容前面或后面添加指定内容
suffixOverrides|prefixOverrides
:将 trim 标签中内容前面或后面去掉指定内容 - 若标签中没有内容时,trim 标签也没有任何效果。
比方说我们要实现根据条件查询多个员工信息的需求:
- 使用 if 标签来实现,实现方式如下:
-
判断条件中判断的是属性名(从后台预编译的 sql 语句中传过去的),语句中的应该是
字段名 = #{属性名}
,前面是放到数据库中执行的语句,后面访问的是实体类中的属性名字; -
这里为什么 where 后面要加
1=1
这个条件呢? -
当 where 后面的所有语句都为空的时候,多出来的 where 关键字会报错;
-
或者,当 where 后面 empName 条件为空字符串的时候,这个时候 where 关键字就会和 and 关键字或者 or 关键字连接在一起,造成错误。
- 使用 where 标签和 if 标签实现:
- 使用 where 标签会自动判断后面是否有内容,自动去掉 and 或者 or 关键字,甚至不生成 where 关键字;
- 但是对于放在语句后面的 and 或者 or 是没办法去掉的,如下图所示,这样当 email 属性为空字符串的时候 sql 语句的最后就会带一个 and ,然后报错:
- 使用 trim 标签和 if 标签实现:
- 当 trim 标签内部有 sql 语句的时候,即任意一个条件被定义的话,那么我们就在前面加上
where
字段; - 当 trim 标签内部的 sql 语句是以
and/or
结尾的时候,那么就将其去掉。
10.2 choose - when - otherwise 标签
4、choose、when、otherwise,相当于if…else if…else
注意:
- when 至少要有一个,otherwise 最多只能有一个
实现根据某一个条件来实现查询员工信息:
测试一下,这边所有的条件啊都设置为空:
//测试choose、when、otherwise
@Test
public void testGetEmpByChoose(){
SqlSession sqlSession = SqlSessionUtils.getSqlSession();
DynamicSQLMapper mapper = sqlSession.getMapper(DynamicSQLMapper.class);
List<Emp> list = mapper.getEmpByChoose(new Emp(null, "", null, "", ""));
System.out.println(list);
}
那么结果应该就是根据 did=1
这个条件来查询,查询出来的应该是赵六,我们没有进行表连接,所以查询出来的员工信息不包含 dept 属性:
10.3 foreach 标签
5、foreach:
- collection:设置需要循环的数组或集合;
- item:表示数组或集合中的每一个数据
- separator:循环体之间的分割符,默认前后会添加空格,不用自己手动添加
- open:foreach 标签所循环的所有内容的开始符
- close:foreach 标签所循环的所有内容的结束符
比如我们想要实现 批量删除或者批量添加 功能,那么我们的 sql 语句应该是:
where id in (1,2,3)
where id = 1 or id = 2 or id = 3
insert into t_user values(?,?,?),(?,?,?),(?,?,?)
mapper 接口中定义:
- 使用
@Param("eids")
注解来规定底层 map 容器中放置数组的键值对的 key 的名称;
- 插入数据的时候传输过来的参数是 list 集合,先取出其中的每一个 emp 对象,然后通过
对象.属性
的方式提取属性值; - 这就有点像那个 java 类中的 for 用法:
测试类:
//测试批量删除
@Test
public void testDeleteMoreByArray(){
SqlSession sqlSession = SqlSessionUtils.getSqlSession();
DynamicSQLMapper mapper = sqlSession.getMapper(DynamicSQLMapper.class);
int result = mapper.deleteMoreByArray(new Integer[]{6, 7, 8});
System.out.println(result);
}
//测试批量插入
@Test
public void testInsertMoreByList(){
SqlSession sqlSession = SqlSessionUtils.getSqlSession();
DynamicSQLMapper mapper = sqlSession.getMapper(DynamicSQLMapper.class);
Emp emp1 = new Emp(null,"a1",23,"男","123@qq.com");
Emp emp2 = new Emp(null,"a2",23,"男","123@qq.com");
Emp emp3 = new Emp(null,"a3",23,"男","123@qq.com");
List<Emp> emps = Arrays.asList(emp1, emp2, emp3);
System.out.println(mapper.insertMoreByList(emps));
}
上面 mapper 映射文件中三种 sql 语句编写方式对应的 sql 语句,输出的结果为:
注意:
- 为什么这里使用
#{}
这种占位符没事儿呢,之前不是说批量删除不能用 占位符赋值方法吗? - 因为上面 8.2 节是一个占位符传 3 个数字,这里是三个占位符分别传 3 个数字,你看下面这句 sql 语句可以正常执行是不是。
10.4 SQL 片段
6、sql 标签
- 设置SQL片段:
<sql id="empColumns">eid,emp_name,age,sex,email</sql>
- 引用SQL片段:
<include refid="empColumns"></include>
11、MyBatis 的缓存
11.1 MyBatis 的一级缓存
一级缓存是 SqlSession 级别的,通过同一个 SqlSession 查询的数据会被缓存,下次查询相同的数据,就会从缓存中直接获取,不会从数据库重新访问。
11.1.1 一级缓存生效(默认生效)
测试一级缓存:
先看一下对于 mapper 接口的不同的 mapper 实现对象,这个时候是同一个 SqlSession:
//测试一级缓存,默认开启的
@Test
public void testOneCache(){
SqlSession sqlSession1 = SqlSessionUtils.getSqlSession();
CacheMapper mapper1 = sqlSession1.getMapper(CacheMapper.class);
Emp emp1 = mapper1.getEmpByEid(1);
System.out.println(emp1);
CacheMapper mapper2 = sqlSession1.getMapper(CacheMapper.class);
Emp emp2 = mapper2.getEmpByEid(1);
System.out.println(emp2);
}
- 可以看出来 sql 语句预编译和参数值穿了一次,也只执行了一次。
11.1.2 一级缓存失效
测试第一种失效情况:
//测试一级缓存,默认开启的
@Test
public void testOneCache(){
SqlSession sqlSession1 = SqlSessionUtils.getSqlSession();
CacheMapper mapper1 = sqlSession1.getMapper(CacheMapper.class);
Emp emp1 = mapper1.getEmpByEid(1);
System.out.println(emp1);
SqlSession sqlSession2 = SqlSessionUtils.getSqlSession();
CacheMapper mapper2 = sqlSession2.getMapper(CacheMapper.class);
Emp emp2 = mapper2.getEmpByEid(1);
System.out.println(emp2);
}
测试第三、四种失效情况(插入数据,手动清空缓存):
//测试一级缓存,默认开启的
@Test
public void testOneCache(){
SqlSession sqlSession1 = SqlSessionUtils.getSqlSession();
CacheMapper mapper1 = sqlSession1.getMapper(CacheMapper.class);
Emp emp1 = mapper1.getEmpByEid(1);
System.out.println(emp1);
mapper1.insertEmp(new Emp(null,"abc",23,"男","123@qq.com"));
// sqlSession1.clearCache();
Emp emp2 = mapper1.getEmpByEid(1);
System.out.println(emp2);
}
11.2 MyBatis 的二级缓存
二级缓存是 SqlSessionFactory 级别,通过同一个 SqlSessionFactory 创建的 SqlSession 查询的结果会被缓存;此后若再次执行相同的查询语句,结果就会从缓存中获取。
11.2.1 二级缓存开启
-
在核心配置文件中,设置全局配置属性 cacheEnabled=“true”,默认为 true,不需要设置;
-
在映射文件中设置标签
<cache />
;
-
二级缓存必须在 SqlSession 关闭或提交之后有效;
-
查询的数据所转换的实体类类型必须实现序列化的接口。
测试二级缓存,同一个 sqlSessionFactory ,但是不同的 sqlSession 试一下,注意需要进行 sqlSession 提交或者关闭才生效:
//测试二级缓存
@Test
public void testTwoCache(){
try {
//加载核心配置文件为字节输入流,根据输入流创建一个 sqlSessionFactory,从中获取一个 sqlSession
InputStream is = Resources.getResourceAsStream("mybatis-config.xml");
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(is);
SqlSession sqlSession1 = sqlSessionFactory.openSession(true);//打开事务自动提交功能
CacheMapper mapper1 = sqlSession1.getMapper(CacheMapper.class);
System.out.println(mapper1.getEmpByEid(1));
sqlSession1.close();
SqlSession sqlSession2 = sqlSessionFactory.openSession(true);
CacheMapper mapper2 = sqlSession2.getMapper(CacheMapper.class);
System.out.println(mapper2.getEmpByEid(1));
} catch (IOException e) {
e.printStackTrace();
}
}
注意:
- 这里的
sqlSessionFactory.openSession(true);
是开启了事务的自动提交而不是 sqlSession 的提交。
- 缓存命中了,没有执行 sql 预编译和传参数,即没有发生和数据库的交互。
11.2.2 二级缓存失效
11.3 MyBatis 缓存查询的顺序
- 二级缓存,当我们没有关闭或者提交 sqlSession 的时候,查询的数据会保存到一级缓存中;
- 当我们关闭或者提交之后,查询的数据会保存到二级缓存中。
11.4 整合第三方缓存 EHCache 代替二级缓存(一级缓存不能替换)
看笔记(打印信息更多了)
12、MyBatis 的逆向工程
12.1 创建逆向工程的步骤
- 添加依赖和插件,这里的 mybatis 和 JDBC 换成你自己对应的版本:
<!-- 依赖MyBatis核心包 -->
<dependencies>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.10</version>
</dependency>
<!-- 控制Maven在构建过程中相关配置 -->
<build>
<!-- 构建过程中用到的插件 -->
<plugins>
<!-- 具体插件,逆向工程的操作是以构建过程中插件形式出现的 -->
<plugin>
<groupId>org.mybatis.generator</groupId>
<artifactId>mybatis-generator-maven-plugin</artifactId>
<version>1.3.0</version>
<!-- 插件的依赖 -->
<dependencies>
<!-- 逆向工程的核心依赖 -->
<dependency>
<groupId>org.mybatis.generator</groupId>
<artifactId>mybatis-generator-core</artifactId>
<version>1.3.2</version>
</dependency>
<!-- 数据库连接池 -->
<dependency>
<groupId>com.mchange</groupId>
<artifactId>c3p0</artifactId>
<version>0.9.2</version>
</dependency>
<!-- MySQL驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.29</version>
</dependency>
</dependencies>
</plugin>
</plugins>
</build>
- 创建 MyBatis 的核心配置文件
mybatis-config.xml
; - 创建逆向工程的配置文件,文件名必须是:
generatorConfig.xml
(修改数据库名称和密码、修改包名、修改对应的数据库表名)
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE generatorConfiguration
PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"
"http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">
<generatorConfiguration>
<!--
targetRuntime: 执行生成的逆向工程的版本
MyBatis3Simple: 生成基本的CRUD(清新简洁版),只有增删改查(查所有和根据 Id 查)5个方法
MyBatis3: 生成带条件的CRUD(奢华尊享版)
-->
<context id="DB2Tables" targetRuntime="MyBatis3">
<!-- 数据库的连接信息 -->
<jdbcConnection driverClass="com.mysql.cj.jdbc.Driver"
connectionURL="jdbc:mysql://localhost:3306/mybatis"
userId="root"
password="abc123">
</jdbcConnection>
<!-- javaBean的生成策略-->
<javaModelGenerator targetPackage="com.atguigu.mybatis.pojo"
targetProject=".\src\main\java">
<!--true生成的是子包,false生成的是一个文件夹名-->
<property name="enableSubPackages" value="true"/>
<property name="trimStrings" value="true"/>
</javaModelGenerator>
<!-- SQL映射文件的生成策略 -->
<sqlMapGenerator targetPackage="com.atguigu.mybatis.mapper"
targetProject=".\src\main\resources">
<property name="enableSubPackages" value="true"/>
</sqlMapGenerator>
<!-- Mapper接口的生成策略 -->
<javaClientGenerator type="XMLMAPPER"
targetPackage="com.atguigu.mybatis.mapper" targetProject=".\src\main\java">
<property name="enableSubPackages" value="true"/>
</javaClientGenerator>
<!-- 逆向分析的表 -->
<!-- tableName设置为*号,可以对应所有表,此时不写domainObjectName -->
<!-- domainObjectName属性指定生成出来的实体类的类名,会自动在后面补上Mapper -->
<table tableName="t_emp" domainObjectName="Emp"/>
<table tableName="t_dept" domainObjectName="Dept"/>
</context>
</generatorConfiguration>
解释:
- MyBatis3Simple: 生成基本的CRUD(清新简洁版),只有增删改查(查所有和根据 Id 查)5个方法
- MyBatis3: 生成带条件的CRUD(奢华尊享版)
- 执行 MBG 插件的 generate 目标:
12.2 豪华尊享版两种函数的区别:
12.3 QBC 查询
@Test
public void testMBG(){
try {
InputStream is = Resources.getResourceAsStream("mybatis-config.xml");
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(is);
SqlSession sqlSession = sqlSessionFactory.openSession(true);
EmpMapper mapper = sqlSession.getMapper(EmpMapper.class);
//查询所有数据
// List<Emp> list = mapper.selectByExample(null);
// list.forEach(emp -> System.out.println(emp));
//根据条件查询
// EmpExample example = new EmpExample();
// example.createCriteria().andEmpNameEqualTo("张三").andAgeGreaterThan(20);
// example.or().andDidIsNotNull();
// List<Emp> list = mapper.selectByExample(example);
// list.forEach(emp -> System.out.println(emp));
//有选择性的修改,那么属性值为 null 的值对应的字段不会进行修改
mapper.updateByPrimaryKeySelective(new Emp(1,"admin",22,null,"456@qq.com",3));
} catch (IOException e) {
e.printStackTrace();
}
}
- and 条件都是链式添加的;
- or 条件需要换行,调用 or() 方法之后再调用 and() 方法。
13、分页插件
13.1 分页插件使用步骤
- 在 pom.xml 种添加依赖
<!-- https://mvnrepository.com/artifact/com.github.pagehelper/pagehelper -->
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper</artifactId>
<version>5.2.0</version>
</dependency>
- 在
mybatis-config.xml
种配置分页插件
<plugins>
<!--设置分页插件-->
<plugin interceptor="com.github.pagehelper.PageInterceptor"></plugin>
</plugins>
13.2 分页插件的使用(不用自己实现分页功能)
- 实现分页的 sql 语句
limit index,pageSize
- index:当前页的起始索引,起始数据的条数 - 1
- pageSize:每页显示的条数
- pageNum:当前页的页码
- index = (pageNum-1) * pageSize
测试:
@Test
public void testPageHelper(){
try {
InputStream is = Resources.getResourceAsStream("mybatis-config.xml");
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(is);
SqlSession sqlSession = sqlSessionFactory.openSession(true);
EmpMapper mapper = sqlSession.getMapper(EmpMapper.class);
//开启分页功能
// Page<Object> page = PageHelper.startPage(2, 4);
PageHelper.startPage(2, 4);
//查询所有的数据
List<Emp> list = mapper.selectByExample(null);
//展示导航分页的页码数,一般都是奇数
PageInfo<Emp> page = new PageInfo<>(list, 5);
System.out.println(page);
} catch (IOException e) {
e.printStackTrace();
}
}
Page<Object> page = PageHelper.startPage(2, 4);
,这个函数返回的 page 是包含少量信息的 page数据;PageInfo<Emp> page = new PageInfo<>(list, 5);
,展示非常详细的 pageInfo 信息,里面包含 page 信息。
常用数据:
- pageNum:当前页的页码
- pageSize:每页显示的条数
- size:当前页显示的真实条数(有可能每页显示 5 条,但是最后一页只剩 2 条数据这样子)
- total:总记录数
- pages:总页数
- prePage:上一页的页码
- nextPage:下一页的页码
- isFirstPage/isLastPage:是否为第一页/最后一页
- hasPreviousPage/hasNextPage:是否存在上一页/下一页
- navigatePages:导航分页的页码数
- navigatepageNums:导航分页的页码,[1,2,3,4,5]