JDBC 的缺陷:
Connection, preparedStatement, ResultSet
对象的创建与释放ResultSet
的查询结果,需要自己封装为 List
Hibernate-数据库交互的框架(Object Relation Mapping,ORM),他将整个与 MySQL 操作有关的逻辑都封装好了。我们在编写业务的时候,不用管MySQL的具体操作。
所以我们现希望:一个能支持定制化SQL,同时SQL语句也不要硬编码在Java程序中,并有着强大的功能。
PreparedStatement
: ps= connection.preparedStatement(sql)
,将参数化的SQL语句传给数据库进行预编译。ps.setObject(i+1, ars[i])
ps.excute()
中文文档地址
MyBatis
是 SQL Mapper Framework for Java(SQL 映射框架),是一个强化版的 JDBC,即:
SQL
映射
Data Access Objects (DAOs)
: 数据访问
MyBatis 底层就是对原生 JDBC 的一个简单封装。但其,将SQL硬编码在Java程序中这一部分抽取出来,改写在配置文件中。实现了 SQL 和 Java 编码分开的效果,功能边界清晰,一个专注业务,一个专注数据。
其余的步骤则封装成一个整体。
这样就完全解决了数据库优化问题,在带来了便捷性还保证了灵活性。
MyBatis 提供了哪些功能:
Connection, Statement(PreparedStatement), ResultSet
的能力,不在需要我们来创建这些对象了sql
语句的能力,不用我们自己执行 sql
sql
,把 sql
查询结果转化为 Java
对象,List
集合的能力因此,我们只需要提供 SQL 语句。流程是:
sql
语句MyBatis
处理该语句JavaBean
对象或 List
集合基础环境搭建:
创建一个数据库,其内有一个 user
表,字段如下:
id
name
password
address
phone
创建一个对应的 JavaBean:User
创建 UserDAO
接口
public interface UserDAO {
User getUserById(Integer id);
}
MyBatis 开始:
导入三个包:
mybatis-3.5.7.jar
mysql-connector-java-8.0.23.jar
log4j-1.2.17
:日志包,依赖于 classpath
目录下的 log4j.xml
。(不需要日志的话,可不用)写配置:
第一个配置文件:mybatis-config.xml
,其作为 MyBatis 的 全局配置文件,配置了事务管理器,和dataSource。目的是:
第二个配置文件:编写 DAO 接口的实现配置文件,相当于 DAOImpl。主要实现以下几个地方
1、namespace 属性:告诉 Mabits 当前配置文件用于实现哪个接口类
2、重写接口类中的方法,如: 标签用于重写查询类的方法,其中:
#{变量名}
代替。表示从传入参数中,取出变量名的值。
DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="top.soultop.dao.UserDAO">
<select id="getUserById" resultType="top.soultop.bean.User">
select *
from user
where id = #{id}
select>
mapper>
在全局配置文件中,注册第二步的标签文件。最终的全局配置文件如下:
DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/test?rewriteBatchedStatements=true"/>
<property name="username" value="root"/>
<property name="password" value="*******"/>
dataSource>
environment>
environments>
<mappers>
<mapper resource="UserDAO.xml"/>
mappers>
configuration>
测试:
SqlSessionFactory
,用于创建 SqlSession
SqlSession
用于操作数据库DAO
实现类public class MyBatisTest {
@Test
public void tt() throws Exception{
// 1. 从 XML 中构建 SqlSessionFactory
// SqlSession 工厂,用于创建 SqlSession对象 -- 类似于 Connection
InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
// 2. 从 SqlSessionFactory 中获取 SqlSession(相当于getConnection())
SqlSession openSession = sqlSessionFactory.openSession();
try {
//3. 获取到DAO接口的实现
UserDAO userDAOMapper = openSession.getMapper(UserDAO.class);
// 可以发现,userDAOMapper 中,可以调用 UserDAO 接口的实现。
User userById = userDAOMapper.getUserById(1);
System.out.println(userById);
} finally {
// 关闭资源
openSession.close();
}
}
}
/**
DEBUG 05-14 20:46:22,214 ==> Preparing: select * from user where id = ? (BaseJdbcLogger.java:137) 执行的sql语句
DEBUG 05-14 20:46:22,251 ==> Parameters: 1(Integer) (BaseJdbcLogger.java:137) 参数填充过程
DEBUG 05-14 20:46:22,292 <== Total: 1 (BaseJdbcLogger.java:137) 查询结果的数量
User{id=1, name='章子怡', password='qwerty', address='Beijing', phone='13788658672'}
*/
配置文件部分:
先写 MyBatis 的 全局配置文件,用于配置事务管理器,和 dataSource。作用是,指导 MyBatis 连接数据库的配置,及如何操作数据库
:数据源配置
:引入 接口实现的 配置文件为 DAO 接口编写配置文件,相当于 DAOImpl。格式如下:
<mapper namespace="top.soultop.dao.UserDAO">
<select id="对应接口的方法名" resultType="指定返回值类型">
select *
from user
where id = #{参数名}
select>
<insert id="对应接口的方法名">
INSERT
INTO user (name, password, address, phone)
VALUES (#{name}, #{password}, #{address}, #{phone})
insert>
<delete id="对应接口的方法名">
delete>
<update id="对应接口的方法名">
update>
<select id="getUserCount" resultType="java.lang.Integer">
SELECT COUNT(*)
FROM user
select>
mapper>
MyBatis操作部分:
先从 XML 中构建 SqlSessionFactory,用于创建 SqlSession对象(类似于 Connection)整个项目 new 一次就行
InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
获取 SqlSession: sqlSessionFactory.openSession()
。 和数据库每进行一次会话就 新建一个SqlSession
boolean
参数,用于指定 是否自动提交,默认是false不自动提交从 Session
中获取需要映射的DAO类:
UserDAO userDAO = openSession.getMapper(UserDAO.class);
执行方法:
userDAO.insertUser(new User(0, "zyLee", "1234", "哈尔滨工程大学", "15191776038"));
由于,MyBatis 在默认情况下是 非自动提交的,所以最终需要手动提交:
try {
//...
} finally {
openSession.commit(); // 提交
openSession.close();
}
其也可以在获取
sqlSession
时,通过传入true
参数来改为自动提交。
MyBatis 的 全局配置文件 包含了会深深影响 MyBatis 行为的设置和属性信息。 配置文档的顶层结构如下:
configuration(配置)
properties
:与 Spring 中的 property-placeholder
一样,都是用于引用外部文件。其格式为:
,其中:
resource
:从 classpath 找
url
:从磁盘找
settings
:这是 MyBatis 中极为重要的调整设置,它们会改变 MyBatis 的运行时行为。
mapUnderscoreToCamelCase
:是否开启 驼峰命名规则 映射。即将数据库中的 a_b
自动映射(而不用起别名)为 JavaBean 中的 aB
属性名。
typeAliases
:为类别其别名,后续使用时就不用写全类名,方便在MyBatis后续的引用
为单个JavaBean起别名
<typeAliases>
<typeAlias type="top.soultop.bean.User" alias="User"/>
typeAliases>
为整个JavaBean包起别名,默认为 JavaBean 类名
<typeAliases> <package name="top.soultop.bean"/>typeAliases>
如果批量产生类名后,还想自定义某些类的别名。则在对应的JavaBean类上使用@Alias("newName")
注解。
environments
(环境配置):
environment
配置一个具体的环境,每个具体的环境都需要一个事务管理器,和一个数据源
:JDBC
:但是,Spring做事务管理比 MyBatis 的 JDBC 要强。druid
做数据源管理也比 MyBatis 的强。所以后续我们几乎是 不使用 MyBatis 的 environments
的。
databaseIdProvider
(数据库厂商标识):用于做数据库移植的(如,MySQL项目,要迁移到Oracle)。
<databaseIdProvider type="DB_VENDOR"> <property name="MySQL" value="mysql"/> <property name="Oracle" value="oracle"/> <property name="SQL Server" value="sqlserver"/>databaseIdProvider>
此时,在写接口的实现配置文件时,就可以指定不同数据库的语句了:
<select id="getUserCount" resultType="java.lang.Integer" databaseId="mysql"> SELECT COUNT(*) FROM userselect><select id="getUserCount" resultType="java.lang.Integer" databaseId="oracle"> SELECT COUNT(*) FROM userselect><select id="getUserCount" resultType="java.lang.Integer" databaseId="sqlserver"> SELECT COUNT(*) FROM userselect>
:
url:磁盘下找 sql 映射文件
resource:在类路径下找 sql 映射文件
class:用接口的全类名注册,
注: 使用 class
时,还可以用注解实现接口。但是 MyBatis 一般不推荐这样使用。因为相当于又硬编码在了 Java 源码里
/** * 基于注解的接口实现。虽然简单,但不常用。 * 但是不常用,因为又造成了 SQL 语句硬编码在 Java 源文件的情况,强耦合。 */public interface UserDAOAnnotation extends UserDAO { @Select("SELECT * FROM user" + " WHERE" + " id = #{id}") User getUserById(Integer id); @Select("SELECT COUNT(*)" + " FROM user") Integer getUserCount();}
推荐
class
型的和resource
型的配合使用。即将一些不重要的、简短的 SQL 语句使用注解实现。
:批量注册 ★★★
以后就都用这个了,一次能把 dao 都导入。
cache
– 该命名空间的缓存配置。cache-ref
– 引用其它命名空间的缓存配置。resultMap
– 描述如何从数据库结果集中加载对象,是最复杂也是最强大的元素。parameterMap
– 老式风格的参数映射。此元素已被废弃,并可能在将来被移除!请使用行内参数映射。文档中不会介绍此元素。sql
– 可被其它语句引用的可重用语句块。insert, update, delete, select
– 映射 CRUD 语句。属性 | 描述 |
---|---|
id ★★★ |
在命名空间中唯一的标识符,可以被用来引用这条语句。 |
parameterType |
将会传入这条语句的参数的类全限定名或别名。这个属性是可选的,因为 MyBatis 可以通过类型处理器(TypeHandler)推断出具体传入语句的参数,默认值为未设置(unset)。(一般不用写) |
parameterMap |
用于引用外部 parameterMap 的属性,目前 已被废弃。请使用行内参数映射和 parameterType 属性。 |
flushCache ★★★ |
将其设置为 true 后,只要语句被调用,都会导致本地缓存和二级缓存被清空,默认值:(对 insert、update 和 delete 语句)true。 |
timeout |
这个设置是在抛出异常之前,驱动程序等待数据库返回请求结果的秒数。默认值为未设置(unset)(依赖数据库驱动)。(可用Spring管理,一般不设置) |
statementType |
可选 STATEMENT,PREPARED 或 CALLABLE。这会让 MyBatis 分别使用 Statement ,PreparedStatement 或 CallableStatement ,默认值:PREPARED 。(一般不调整) |
useGeneratedKeys ★ |
(仅适用于 insert 和 update)这会令 MyBatis 使用 JDBC 的 getGeneratedKeys 方法来 取出由数据库内部生成的主键(比如:像 MySQL 和 SQL Server 这样的关系型数据库管理系统的自动递增字段),默认值:false。 |
keyProperty ★ |
(仅适用于 insert 和 update)指定能够唯一识别对象的属性,MyBatis 会使用 getGeneratedKeys 的返回值或 insert 语句的 selectKey 子元素设置它的值,默认值:未设置(unset )。如果生成列不止一个,可以用逗号分隔多个属性名称。 |
keyColumn |
(仅适用于 insert 和 update)设置生成键值在表中的列名,在某些数据库(像 PostgreSQL)中,当主键列不是表中的第一列的时候,是必须设置的。如果生成列不止一个,可以用逗号分隔多个属性名称。(一般不用) |
databaseId |
如果配置了数据库厂商标识(databaseIdProvider),MyBatis 会加载所有不带 databaseId 或匹配当前 databaseId 的语句;如果带和不带的语句都有,则不带的会被忽略。指定当前sql语句作用于哪个数据库下 |
获取要插入数据的自增字段的值:
对于支持自增主键的数据库(MySQL 和 SQL Server):
<insert id="insertUser" useGeneratedKeys="true" keyProperty="id"> INSERT INTO user (name, password, address, phone) VALUES (#{user.name}, #{user.password}, #{user.address}, #{user.phone})insert>
这里配置的作用是:
获取新插入元素在数据库中自增字段的值(
useGeneratedKeys="true"
)并将值封装给新插入元素 user 的 id(
useGeneratedKeys="true"
) 属性
对于不支持自增主键的数据库:
<insert id="insertUser"> <selectKey order="BEFORE" keyProperty="id" resultType="int"> SELECT MAX(id)+1 FROM test.user selectKey> INSERT INTO user (id, name, password, address, phone) VALUES (#{id}, #{name}, #{password}, #{address}, #{phone})insert>
这里配置的作用是:
在主插入语句 运行之前(
order="BEFORE"
)查询出当前表中最大的 id+1。
并将其,赋值给
id
(keyProperty="id"
)然后再执行插入语句,这样插入语句就能有一个 id 值供给给插入。
传入 单个 参数:
#{随便写}
都能取值成功。#{属性名}
。注:#{对象名.属性名}
是错误的。传入 多个 参数:
用参数名传递#{参数名}
无效。而是要写 #{0}
或 #{param1}
即传入索引。
User getUserByIdAndName(Integer id, String name);
<select id="getUserByIdAndName" resultType="top.soultop.bean.User"> SELECT * FROM user WHERE id = #{0} AND name=#{1}select>
原因:传入多个参数时,MyBatis 会自动封装这些参数在一个
Map
中,封装使用的 key 就是参数的索引和参数的第几个表示。形如:map.put(0, 传入的值)
这样就不利于理解代码的意义。所以我们可以 使用
@Param
注解,来告诉MyBatis封装Map时,用的key。
User getUserByIdAndName(@Param("id") Integer id, @Param("name") String name);
<select id="getUserByIdAndName" resultType="top.soultop.bean.User"> SELECT * FROM user WHERE id = #{id} AND name=#{name}select>
传入 JavaBean:直接使用 #{属性名}
。注:#{对象名.属性名}
是错误的。
传入 map:#{key}
取值。
User getUserByMap(Map<String, Object> m);
总结:
MyBatis 会在 传入多个参数 的情况下,自动将参数封装成一个 Map
。再一个例子:
voi m(@Param("id")Integer id, String name, User user);
,取值:
#{id}
#{param2}
#{param3.phone}
。推荐要传入多个参数时,封装成一个Map传入。
MyBatis在传入单个 JavaBean
时,给SQL赋值时直接使用 #{pojo属性名}
即可。如果想要使用形如: #{user.name}
,则必须给方法声明时的参数加 @Param("user")
的注解。
传入单个基本类型参数时,#{随意}
都能取出值。
默认情况下,使用 #{}
参数语法时,MyBatis 会创建 PreparedStatement
参数占位符,并通过占位符安全地设置参数(就像使用 ? 一样)。 这样做更安全,更迅速,通常也是首选做法。id=#{id} and name=#{name}
就是: WHERE id = ? and name=?
但是,SQL 只接受在参数位置处插入占位符。而有时候我们想直接在 SQL 语句中直接插入一个不转义的字符串。比如,我们想用同一个方法实现根据不同的参数查询:
select * from <$> where <$> = <#>}
@Select("select * from user where ${column} = #{value}") // 对 #{column} 进行简单的字符串替换User findByColumn(@Param("column") String column, @Param("value") String value);// 应用时就可以是:findByColumn(id, 3);select * from user where id = ? // 传入 SQL 进行预编译findByColumn(name, "Jack");select * from user where name = ? // 传入 SQL 进行预编译
如果不好理解,就这么记:
在支持参数预编译的位置使用
#{}
不支持预编译的地方用
${}
在简单的场景下,MyBatis 可以为你自动映射查询结果。当数据库字段名单词间使用下划线分隔时(如 a_b),为使其与JavaBean中遵守驼峰命名规则的属性名(aB)自动映射,需要在全局配置文件的
中将 mapUnderscoreToCamelCase
设置为 true。
但当数据库字段名和JavaBean的属性名 完全不对应 时,或:起别名或使用 结果映射。
此外,当 JavaBean 互相包含时,也已经不是简单的起别名能应付了的。
constructor
- 用于在实例化类时,注入结果到构造方法中
idArg
- ID 参数;标记出作为 ID 的结果可以帮助提高整体性能arg
- 将被注入到构造方法的一个普通结果id
– 一个 ID 结果;标记出作为 ID 的结果可以帮助提高整体性能result
– 注入到字段或 JavaBean 属性的普通结果association
– 一个复杂类型的关联;许多结果将包装成这种类型
resultMap
元素,或是对其它结果映射的引用collection
– 一个复杂类型的集合
resultMap
元素,或是对其它结果映射的引用discriminator
– 使用结果值来决定使用哪个 resultMap
case
也是一个结果映射,因此具有相同的结构和元素;或者引用其它的结果映射下面是一个使用结果映射的例子:
JavaBean
// Lockpublic class Lock{ private Integer id; private String lockName;}// Key 中含有 Lock Beanpublic class Key{ private Integer id; private String keyName; // 当前钥匙所能开的锁 private Lock lock;}
此处一个 lock 只和一个 key 对应。
数据库:
key 表:
列名 | 数据类型 | 属性 |
---|---|---|
id | int | 主键、非空、自增 |
keyname | varchar(100) | |
lockid | int | 外键(关联lock的id列名) |
lock表:
列名 | 数据类型 | 属性 |
---|---|---|
id | int | 主键、非空、自增 |
lockname | varchar(100) | |
KeyDAO.java
public interface KeyDAO{ /** * 将钥匙和锁子信息一起查出 */ public Key getKeyById(Integer id);}
外联查询的SQL语句,及其结果
SELECT k.*,l.* FROM test.key kLEFT JOINtest.lock lONk.lockid=l.idWHEREk.id=1;
id | keyname | lockid | id | lockname |
---|---|---|---|---|
1 | 1号钥匙 | 1 | 1 | 1号锁 |
映射文件xml:
<mapper namespace="top.soultop.dao.KeyDAO"> <resultMap id="KLMap" type="top.soultop.bean.Key"> <id property="id" column="id"/> <result property="keyName" column="keyname"/> <association property="lock" javaType="top.soultop.bean.Lock"> <id property="id" column="id"/> <result property="lockName" column="lockname"/> association> resultMap> <select id="getKeyById" resultMap="KLMap"> SELECT k.*, l.* FROM test.key k LEFT JOIN test.lock l ON k.lockid = l.id WHERE k.id = #{id}; select>mapper>
测试
@Testpublic void getKeyById() throws Exception{ initSessionFactory(); SqlSession session = sqlSessionFactory.openSession(); try{ KeyDAO mapper = session.getMapper(KeyDAO.class); Key keyById = mapper.getKeyById(1); System.out.println(keyById); } finally { session.close(); }}
建议:
整体赋值。P256-P259 可以再看一下
JavaBean
// Lockpublic class Lock{ privete Integer id; privete String lockName; // 当前锁对应多把钥匙 private List keys;}// Key 中含有 Lock Beanpublic class Key{ privete Integer id; privete String keyName; // 当前钥匙所能开的锁 private Lock lock;}
从 Key 的视角来看:一个 key 对应 一个 lock(1v1)。
从 Lock 的视角来看:一个 lock 对应多个 key(1vN)。
从数据库分析:
1vN 时,外键应该放在N的这边
NvN时,由中间表存储对应关系
DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"><mapper namespace="top.soultop.dao.LockDAO"> <resultMap id="LKMap" type="top.soultop.bean.Lock"> <id property="id" column="id"/> <result property="lockName" column="lockname"/> <collection property="keys" ofType="top.soultop.bean.Key"> <id property="id" column="id"/> <result property="keyname" column="keyname"/> collection> resultMap> <select id="getLockById" resultMap="KLMap"> SELECT l.* k.*, FROM test.lock l LEFT JOIN test.key k ON l.id = k.lockid WHERE l.id = #{id}; select>mapper>
这种写法就比较麻烦了,所以MyBatis 提供了关联嵌套 Select 查询
<resultMap id="KL" type="top.soultop.bean.Key"> <association property="lock" column="lockid" javaType="top.soultop.bean.Lock" select="top.soultop.dao.LockDAO.selectLock"/>resultMap><select id="selectKey" resultMap="KL"> SELECT * FROM test.key WHERE id = #{id}select><select id="selectLock" resultType="top.soultop.bean.Lock"> SELECT * FROM test.lock WHERE ID = #{id}select>
执行步骤:
resultMap
进行封装
select="top.soultop.dao.LockDAO.selectLock"
,调用 selectLock
方法lockid
传递给 selectLock
方法。学到了P256
官方参考文档
public interface UserDAO { /** * 传入多个参数是,MyBatis 会将参数自动封装为一个Map,Key为 paramX,这丧失了原有语义。 * 所以使用 @Param 注释来指定封装Map时的 Key */ User getUserByIdAndName(@Param("id") Integer id, @Param("name") String name); // 加注解,mybatis 对参数封装时就会用这个注解做key /** * 即使返回的是 List,指定 resultType 时也只需要指定 User 的全类名就行。 * MyBatis 会自动封装 * @return */ List getAllUsers(); /** * 将多条记录以 Map 形式返回时,resultType 也指定为 User 的全类名 * * 此外,还需要指定用哪个属性作为 Map 的key。 * @return */ @MapKey("id") Map getAllUsersReturnMap();}
DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"><mapper namespace="top.soultop.dao.UserDAO"> <select id="getAllUsers" resultType="top.soultop.bean.User"> SELECT * FROM user select> <select id="getAllUsersReturnMap" resultType="top.soultop.bean.User"> SELECT * FROM user select> <select id="getUserByIdAndName" resultType="top.soultop.bean.User"> SELECT * FROM user WHERE id = #{id} AND name = #{name} select>mapper>
在全局配置文件中红,使用 settings 设置:
if
标签现有如下方法,根据传入的 User 中所具有的属性,去查询数据库:public List
。
假设现在传入的user
参数具有以下属性:
则 DAO 的实现文件可写为:
<select id="getUserByCondition" resultMap="UserMap">
SELECT * FROM user
WHERE
<if test="id!=null">
id > #{id} AND
if>
<if test="name!=null and !name.equals("")">
name like #{name} AND
if>
<if test="password!=null">
password = #{password}
if>
...
select>
"
是"
的转义字符。
上述 xml 中的动态SQL根据传入的 JavaBean 翻译后就是:SELECT * FROM user WHERE id>1 AND name LIKE Jack AND
可以看到这个SQL语句有问题,由于password判断为空,所以最后多了一个AND。
解决这个问题的办法是:
使用
标签——Mybatis会自动判断,去除多余的AND。
<select id="getUserByCondition" resultMap="UserMap">
SELECT * FROM user
<where>
<if test="id!=null">
id > #{id} AND
if>
<if test="name!=null and !name.equals("")">
name like #{name} AND
if>
<if test="password!=null">
password = #{password}
if>
...
where>
...
select>
使用
处理
<select id="getUserByCondition" resultMap="UserMap">
SELECT * FROM user
<trim prefix="where" prefixOverrides="AND"
suffix="" suffixOverrides="AND">
<if test="id!=null">
id > #{id} AND
if>
<if test="name!=null and !name.equals("")">
name like #{name} AND
if>
<if test="password!=null">
password = #{password}
if>
...
trim>
...
select>
现有如下方法,根据传入 List 中的 id,去查询数据库中id在List中的数据:public List
。
SELECT * FROM user WHERE id IN (1,2,3,4,5)
。 我们在编写代码时,是无法确定传入的 List 集合具体的 size 的。所以可以使用
标签来实现。其内部属性如下:
<foreach collection="ids" item="id_item" separator="," open="(" close=")">
#{id_item}
foreach>