1.MyBatis介绍
1.1 Mybatis由来:
1.2 Mybatis的特点:
Mybatis是一个优秀的持久层框架,它对使用JDBC操作数据库的过程进行了封装,使开发者只关注SQL语句本身,不需要花费精力去处理例如注册驱动、创建connection等jdbc繁杂的过程
1.3 Mbatis在哪写sql语句呢?如何设置sql参数和封装查询结果?
2.Mybatis原理
2.1 JDBC编程的回顾
这里我新建一个工程直接写代码,数据库语句附上
SET FOREIGN_KEY_CHECKS=0;
-- ----------------------------
-- Table structure for `orders`
-- ----------------------------
DROP TABLE IF EXISTS `orders`;
CREATE TABLE `orders` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`user_id` int(11) NOT NULL COMMENT '下单用户id',
`number` varchar(32) NOT NULL COMMENT '订单号',
`createtime` datetime NOT NULL COMMENT '创建订单时间',
`note` varchar(100) DEFAULT NULL COMMENT '备注',
PRIMARY KEY (`id`),
KEY `FK_orders_1` (`user_id`),
CONSTRAINT `FK_orders_id` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ON DELETE NO ACTION ON UPDATE NO ACTION
) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8;
-- ----------------------------
-- Records of orders
-- ----------------------------
INSERT INTO `orders` VALUES ('3', '1', '1000010', '2015-02-04 13:22:35', null);
INSERT INTO `orders` VALUES ('4', '1', '1000011', '2015-02-03 13:22:41', null);
INSERT INTO `orders` VALUES ('5', '10', '1000012', '2015-02-12 16:13:23', null);
-- ----------------------------
-- Table structure for `user`
-- ----------------------------
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(32) NOT NULL COMMENT '用户名称',
`birthday` date DEFAULT NULL COMMENT '生日',
`sex` char(1) DEFAULT NULL COMMENT '性别',
`address` varchar(256) DEFAULT NULL COMMENT '地址',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=27 DEFAULT CHARSET=utf8;
-- ----------------------------
-- Records of user
-- ----------------------------
INSERT INTO `user` VALUES ('1', '王五', null, '2', null);
INSERT INTO `user` VALUES ('10', '张三', '2014-07-10', '1', '北京市');
INSERT INTO `user` VALUES ('16', '张小明', null, '1', '河南郑州');
INSERT INTO `user` VALUES ('22', '陈小明', null, '1', '河南郑州');
INSERT INTO `user` VALUES ('24', '张三丰', null, '1', '河南郑州');
INSERT INTO `user` VALUES ('25', '陈小明', null, '1', '河南郑州');
INSERT INTO `user` VALUES ('26', '王五', null, null, null);
-- ----------------------------
-- Table structure for item
-- ----------------------------
DROP TABLE IF EXISTS `item`;
CREATE TABLE `item` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(32) NOT NULL COMMENT '商品名称',
`price` float(10,1) NOT NULL COMMENT '商品定价',
`detail` text COMMENT '商品描述',
`pic` varchar(64) DEFAULT NULL COMMENT '商品图片',
`createtime` datetime NOT NULL COMMENT '生产日期',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;
-- ----------------------------
-- Records of item
-- ----------------------------
INSERT INTO `item` VALUES ('1', '台式机', '3000.0', '该电脑质量非常好!!!!', null, '2016-02-03 13:22:53');
INSERT INTO `item` VALUES ('2', '笔记本', '6000.0', '笔记本性能好,质量好!!!!!', null, '2015-02-09 13:22:57');
INSERT INTO `item` VALUES ('3', '背包', '200.0', '名牌背包,容量大质量好!!!!', null, '2015-02-06 13:23:02');
jdbc代码:
public class JdbcDemo {
public static void main(String[] args) {
Connection connection=null;
PreparedStatement prepareStatement=null;
ResultSet rs =null;
try {
//1. 注册驱动
Class.forName("com.mysql.jdbc.Driver");
//2. 获得数据库连接
connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/ssm"
+ "?characterEncoding=utf-8", "root", "123456");
//3.定义查询
String sql ="select * from user where id = ? ";
//4.获取预处理statement
prepareStatement = connection.prepareStatement(sql);
//5.设置参数,第一个参数为sql语句中参数的序号(从1开始),第二个参数为设置的参数值
prepareStatement.setInt(1, 1);
//6.发送sql获得结果集
rs = prepareStatement.executeQuery();
//7.遍历结果集
while(rs.next()){
System.out.println(rs.getString("username"));
}
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}finally{
//8 释放资源
if(rs!=null){
try {
rs.close();
} catch (SQLException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
if(prepareStatement!=null){
try {
prepareStatement.close();
} catch (SQLException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
if(connection!=null){
try {
connection.close();
} catch (SQLException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
}
2.2 Mybatis解决了哪些问题?
2.3Mybatis架构
1.加载配置(SqlSessionFactoryBuilder)
a) .SqlMapConfig.xml,此文件作为Mybatis的全局配置文件,配置了Mybatis的运行环境,
b).Mapper.xml 此文件作为Mybatis的sql映射文件,配置了操作数据库的sql语句,此文件需要在全局配置文件中加载。
2.通过加载配置信息构造SqlSessionFactory会话工厂
3.通过会话工厂创建SqlSession会话,我们通过操作SqlSession会话接口对数据库进行crud
4.Mybatis底层定义了Exexutor执行器来具体操作数据库,Executor 接口有两个实现类,一个是基本执行器(默认) 一个是缓存执行器,sqlSession底层是通过Executor接口操作数据库的。
5.Mapped Statement 也是Mybatis一个底层封装对象,它包装了Mybatis配置信息以及sql映射信息等,mapper.xml文件中一个select/insert/update/delete 标签对应一个 MappedStatement 对象,同时select/insert/update/delete 的id也是MappedStatement的id。
a) MappedStatement对sql执行输入参数进行定义,包括HashMap、基本类型、pojo,Executor通过Mapped Statement在执行sql前将输入的java对象映射至sql中,输入参数映射就是jdbc编程中对preparedStatement设置参数。
b) Mapped Statement对sql执行输出结果进行定义,包括HashMap、基本类型、pojo,Executor通过Mapped Statement在执行sql后将输出结果映射至java对象中,输出结果映射过程相当于jdbc编程中对结果的解析处理过程。
3.Mybatis入门程序
3.1 需求
1、根据用户id查询一个用户信息
2、根据用户名称模糊查询用户信息列表
3、添加/更新/删除用户
3.2 环境准备
a). 导入Mybatis.jar包,我这里使用的3.2.7,导入mysql.jar,log4j.jar(非必须).......
b). SqlMapConfig.xml,在classpath config下创建
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<!-- 和spring整合后 environments配置将废除-->
<environments default="development">
<environment id="development">
<!-- 使用jdbc事务管理-->
<transactionManager type="JDBC" />
<!-- 数据库连接池,mybatis本身的连接池-->
<dataSource type="POOLED">
<property name="driver" value="com.mysql.jdbc.Driver" />
<property name="url" value="jdbc:mysql://localhost:3306/ssm?characterEncoding=utf-8" />
<property name="username" value="root" />
<property name="password" value="123456" />
</dataSource>
</environment>
</environments>
</configuration>
SqlMapConfig.xml是mybatis核心配置文件,上边文件的配置内容为数据源、事务管理。
c).在classpath sqlmap下创建UserMapper.xml
namespace 命名 空间,用于隔离sql,后面讲解另一个作用
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="test">
</mapper>
d). 将映射文件加载到主配置文件中
<!-- 加载映射文件 -->
<mappers>
<mapper resource="sqlmap/UserMapper.xml"></mapper>
</mappers>
e).Po类作为mybatis进行sql映射使用,po类通常与数据库表对应,需求对应的PO类User.java如下:
public class User {
private int id;
private String username;// 用户姓名
private String sex;// 性别
private Date birthday;// 生日
private String address;// 地址
get/set……
f). 根据id查询用户
<mapper namespace="test">
<select id="findUserById" parameterType="int" resultType="cn.nanjin.po.User">
select * from user where id =#{NanJin}
</select>
</mapper>
parameterType:定义输入到sql中的映射类型,#{id}表示使用preparedstatement设置占位符号并将输入变量id传到sql。
resultType:定义结果映射类型。因为是普通类型(java基本类型)的查询,所以#{里面写什么都行}
测试类:
public class MybatisTest {
private SqlSessionFactory sqlSessionFactory;
@Before
public void createSqlSessionFactory() throws Exception{
// 配置文件
String resource = "SqlMapConfig.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
// 使用SqlSessionFactoryBuilder从xml配置文件中创建SqlSessionFactory
sqlSessionFactory = new SqlSessionFactoryBuilder()
.build(inputStream);
}
@Test
public void testFindUserById(){
SqlSession sqlSession =null;
try{
//打开session
sqlSession = sqlSessionFactory.openSession();
User u = sqlSession.selectOne("test.findUserById",10);
System.out.println(u);
}catch (Exception e) {
e.printStackTrace();
}finally {
if (sqlSession != null) {
sqlSession.close();
}
}
}
}
2、根据用户名称模糊查询用户信息列表
public void testFindUserByUsername(){
SqlSession sqlSession =null;
try{
//打开session
sqlSession = sqlSessionFactory.openSession();
List<User> selectList = sqlSession.selectList("test.findUserByUserName","小");
System.out.println(selectList);
}catch (Exception e) {
e.printStackTrace();
}finally {
if (sqlSession != null) {
sqlSession.close();
}
}
}
<select id="findUserByUserName" parameterType="string" resultType="cn.nanjin.po.User">
<!-- resultType:结果映射的java类型,指的是单条结果对应的java类型 -->
select * from user where username like '%${value}%'
</select>
这里我们需要注意的是,我没有在使用#{} 而是使用了${value}
使用#{}完成的sql如下
select * from user where username like '%'小'%'
使用${}时的sql如下
select * from user where username like '%小%'
可以知道${}是实现了sql的拼接,类似于普通的statement,而#{}则类似于prepareStatement,防止了注入危险。
小结:
1. #{}和${}
#{}表示一个占位符号,通过#{}可以实现preparedStatement向占位符中设置值,自动进行java类型和jdbc类型转换。#{}可以有效防止sql注入。 #{}可以接收简单类型值或pojo属性值。 如果parameterType传输单个简单类型值,#{}括号中可以是value或其它名称。
${}表示拼接sql串,通过${}可以将parameterType 传入的内容拼接在sql中且不进行jdbc类型转换,不过会引起SQL注入。 ${}可以接收简单类型值或pojo属性值,如果parameterType传输单个简单类型值,${}括号中只能是value。
2. parameterType和resultType
parameterType:指定输入参数的java类型,mybatis通过ognl从输入对象中获取参数值拼接在sql中
resultType:指定输出结果要映射的java类型,mybatis将sql查询结果的一行记录数据映射为resultType指定类型的对象。
3. selectOne和selectList
org.apache.ibatis.exceptions.TooManyResultsException: Expected one result (or null) to be returned by selectOne(), but found: 3
at org.apache.ibatis.session.defaults.DefaultSqlSession.selectOne(DefaultSqlSession.java:70)
selectList可以查询一条或多条记录。
3.新增用户,并返回新增用户id
这里说两种配置
<insert id="insertUserone" parameterType="cn.nanjin.po.User">
<selectKey order="AFTER" resultType="int" keyProperty="id">
SELECT LAST_INSERT_ID()
</selectKey>
INSERT INTO user (username,birthday,sex,address) VALUES
(#{username},#{birthday},#{sex},#{address});
</insert>
public void insertUserone(){
SqlSession sqlSession =null;
try{
//打开session
sqlSession = sqlSessionFactory.openSession();
User u =new User();
u.setAddress("heheh");
u.setBirthday(new Date());
u.setUsername("hifdenf");
sqlSession.insert("test.insertUserone", u);
System.out.println(u.getId());
sqlSession.commit();
}catch (Exception e) {
e.printStackTrace();
}finally {
if (sqlSession != null) {
sqlSession.close();
}
}
这里解释一下xml中的配置,<selectKey>查询主键,order属性取值为AFTER和BEFORE(必须大写否则报错)
<insert id="insertUser" parameterType="cn.nanjin.po.User">
<selectKey order="BEFORE" keyProperty="id" resultType="string">
SELECT UUID()
</selectKey>
INSERT INTO user (id,username,birthday,sex,address) VALUES
(#{id},#{username},#{birthday},#{sex},#{address});
</insert>
4.更新id为10的用户
<update id="updateUser" parameterType="cn.nanjin.po.User">
UPDATE user set username=#{username},address=#{address} WHERE id =#{id}
</update>
//打开session
sqlSession = sqlSessionFactory.openSession();
User u =new User();
u.setAddress("NanJinilove");
u.setUsername("NanJin");
u.setId(10);
sqlSession.update("test.updateUser", u);
System.out.println(u.getId());
sqlSession.commit();
5.删除的用户
<delete id="deleteUser" parameterType="int">
delete from user where id =#{id}
</delete>
增删改别忘了提交事物就Ok了~
4.Mybatis两种开发模式(接口和动态代理)
4.1 SqlSession 适用范围
4.1.1 SqlSessionFactory
4.1.2 SqlSession
SqlSession session = sqlSessionFactory.openSession();
try {
// do work
} finally {
session.close();
}
4.2 接口Dao,主要是为了历史遗留的ibatis,需要编写Dao接口和Dao实现类
public class UserDaoImpl implements UserDao {
private SqlSessionFactory sqlSessionFactory;
public UserDaoImpl(SqlSessionFactory sqlSessionFactory){
this. sqlSessionFactory = sqlSessionFactory;
}
@Override
public User findUserById(int id ) {
SqlSession session = sqlSessionFactory.openSession();
return session.selectOne("test.findUserById", id);
}
@Override
public List<User> findUsers(String name ) {
SqlSession session = sqlSessionFactory.openSession();
return session.selectList("test.findUserByUserName", name );
}
}
@Test
public void selectUserDao(){
UserDaoImpl userDao =new UserDaoImpl(sqlSessionFactory);
User findUserById = userDao.findUserById(10);
System.out.println(findUserById);
List<User> findUsers = userDao.findUsers("小明");
System.out.println(findUsers);
}
}
<mapper namespace="test">
<!-- 根据用户ID查询用户信息 -->
<!-- select/insert/update/delete:对应一个MappedStatement对象 -->
<!-- id:statement的id -->
<!-- #{}:表示占位符?.#{}里面可以指定参数名称 ,如果参数值是简单类型的参数,那么参数名称可以任意 -->
<!-- parameterType:参数类型(Java对象类型) -->
<!-- resultType:结果映射类型(Java对象类型) -->
<select id="findUserById" parameterType="int"
resultType="cn.nanjin.po.User">
SELECT * FROM user WHERE id = #{NanJin}
</select>
<select id="findUserByUserName" parameterType="string" resultType="cn.nanjin.po.User">
select * from user where username like '%${value}%'
</select>
4.2.1 接口方式存在的问题
4.3 Mapper动态代理方式
4.3.1 开发规范
package cn.nanjin.mapper;
import java.util.List;
import cn.nanjin.po.User;
public interface UserMapper {
User findUserById(int id );
List<User> findUserByUserName(String name );
}
<mapper namespace="cn.nanjin.mapper.UserMapper">
<!-- 根据用户ID查询用户信息 -->
<!-- select/insert/update/delete:对应一个MappedStatement对象 -->
<!-- id:statement的id -->
<!-- #{}:表示占位符?.#{}里面可以指定参数名称 ,如果参数值是简单类型的参数,那么参数名称可以任意 -->
<!-- parameterType:参数类型(Java对象类型) -->
<!-- resultType:结果映射类型(Java对象类型) -->
<select id="findUserById" parameterType="int"
resultType="cn.nanjin.po.User">
SELECT * FROM user WHERE id = #{NanJin}
</select>
<select id="findUserByUserName" parameterType="string" resultType="cn.nanjin.po.User">
select * from user where username like '%${value}%'
</select>
</mapper>
@Test
public void findUserByIdMapper(){
SqlSession session = sqlSessionFactory.openSession();
//获取代理对象
UserMapper mapper = session.getMapper(UserMapper.class);
//调用代理对象方法
User user = mapper.findUserById(10);
System.out.println(user);
session.close();
}
4.3.2 小结
namespace
5.SqlMapConfig.xml配置文件
5.1配置文件中的内容和顺序
settings(全局配置参数)
typeAliases(类型别名)
typeHandlers(类型处理器)
objectFactory(对象工厂)
plugins(插件)
environments(环境集合属性对象)
environment(环境子属性对象)
transactionManager(事务管理)
dataSource(数据源)
mappers(映射器)
5.1.1 properties
jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/ssm?characterEncoding=utf-8
jdbc.username=root
jdbc.password=123456
b). 在SqlMapConfig.xml中引用db.properties
<properties resource="db.properties"/>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="${jdbc.driver}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</dataSource>
</environment>
</environments>
当然也可以在properties中定义子标签
<properties>
<property name ="jdbc.username" value="nanjin"/>
</properties>
这里注意一点,Mybatis 按照读取properties内部元素属性---再读取resource或url加载的属性,会覆盖同名属性。
5.1.2 settings(全局配置参数)
<!-- 基本配置如下-->
<settings>
<!-- 该配置影响的所有映射器中配置的缓存的全局开关。默认值true -->
<setting name="cacheEnabled" value="true"/>
<!--延迟加载的全局开关。当开启时,所有关联对象都会延迟加载。 特定关联关系中可通过设置fetchType属性来覆盖该项的开关状态。默认值false -->
<setting name="lazyLoadingEnabled" value="true"/>
<!-- 是否允许单一语句返回多结果集(需要兼容驱动)。 默认值true -->
<setting name="multipleResultSetsEnabled" value="true"/>
<!-- 使用列标签代替列名。不同的驱动在这方面会有不同的表现, 具体可参考相关驱动文档或通过测试这两种不同的模式来观察所用驱动的结果。默认值true -->
<setting name="useColumnLabel" value="true"/>
<!-- 允许 JDBC 支持自动生成主键,需要驱动兼容。 如果设置为 true 则这个设置强制使用自动生成主键,尽管一些驱动不能兼容但仍可正常工作(比如 Derby)。 默认值false -->
<setting name="useGeneratedKeys" value="false"/>
<!-- 指定 MyBatis 应如何自动映射列到字段或属性。 NONE 表示取消自动映射;PARTIAL 只会自动映射没有定义嵌套结果集映射的结果集。 FULL 会自动映射任意复杂的结果集(无论是否嵌套)。 -->
<!-- 默认值PARTIAL -->
<setting name="autoMappingBehavior" value="PARTIAL"/>
<setting name="autoMappingUnknownColumnBehavior" value="WARNING"/>
<!-- 配置默认的执行器。SIMPLE 就是普通的执行器;REUSE 执行器会重用预处理语句(prepared statements); BATCH 执行器将重用语句并执行批量更新。默认SIMPLE -->
<setting name="defaultExecutorType" value="SIMPLE"/>
<!-- 设置超时时间,它决定驱动等待数据库响应的秒数。 -->
<setting name="defaultStatementTimeout" value="25"/>
<setting name="defaultFetchSize" value="100"/>
<!-- 允许在嵌套语句中使用分页(RowBounds)默认值False -->
<setting name="safeRowBoundsEnabled" value="false"/>
<!-- 是否开启自动驼峰命名规则(camel case)映射,即从经典数据库列名 A_COLUMN 到经典 Java 属性名 aColumn 的类似映射。 默认false -->
<setting name="mapUnderscoreToCamelCase" value="false"/>
<!-- MyBatis 利用本地缓存机制(Local Cache)防止循环引用(circular references)和加速重复嵌套查询。
默认值为 SESSION,这种情况下会缓存一个会话中执行的所有查询。
若设置值为 STATEMENT,本地会话仅用在语句执行上,对相同 SqlSession 的不同调用将不会共享数据。 -->
<setting name="localCacheScope" value="SESSION"/>
<!-- 当没有为参数提供特定的 JDBC 类型时,为空值指定 JDBC 类型。 某些驱动需要指定列的 JDBC 类型,多数情况直接用一般类型即可,比如 NULL、VARCHAR 或 OTHER。 -->
<setting name="jdbcTypeForNull" value="OTHER"/>
<!-- 指定哪个对象的方法触发一次延迟加载。 -->
<setting name="lazyLoadTriggerMethods" value="equals,clone,hashCode,toString"/>
</settings>
5.1.3 typeAliases(别名)
别名 | 映射的类型 |
_byte | byte |
_long | long |
_short | short |
_int | int |
_integer | int |
_double | double |
_float | float |
_boolean | boolean |
string | String |
byte | Byte |
long | Long |
short | Short |
int | Integer |
integer | Integer |
double | Double |
float | Float |
boolean | Boolean |
date | Date |
decimal | BigDecimal |
bigdecimal | BigDecimal |
map | Map |
<typeAliases>
<!-- 单个别名定义 -->
<typeAlias alias="user" type="cn.nanjin.po.User"/>
<!-- 批量别名定义,扫描整个包下的类,别名为类名(首字母大写或小写都可以) -->
<package name="cn.nanjin.po"/>
</typeAliases>
<typeAliases>
<!-- 单个别名定义 -->
<typeAlias alias="user" type="cn.nanjin.po.User"/>
<!-- 批量别名定义,扫描整个包下的类,别名为类名(首字母大写或小写都可以) -->
<package name="cn.nanjin.po"/>
</typeAliases>
5.1.4 mappers(映射器)
<!-- resource是使用相对路径方式 -->
<mapper resource="sqlmap/UserMapper.xml"></mapper>
<mapper resource="mapper/UserMapper.xml"></mapper>
<!--使用mapper接口类路径,加载文件 -->
<mapper class="cn.nanjin.mapper.UserMapper"/>
<!-- 直接扫描包 -->
<package name="cn.nanjin.mapper"/>
注意:使用接口类路径/扫描包时,要求mapper接口名称和mapper映射文件名相同,并且放在同一目录
6.Mybatis与Hibernate不同
Mybatis学习门槛低,简单易学,程序员直接编写原生态sql,可严格控制sql执行性能,灵活度高,非常适合对关系数据模型要求不高的软件开发,例如互联网软件、企业运营类软件等,因为这类软件需求变化频繁,一但需求变化要求成果输出迅速。但是灵活的前提是mybatis无法做到数据库无关性,如果需要实现支持多种数据库的软件则需要自定义多套sql映射文件,工作量大。
Hibernate对象/关系映射能力强,数据库无关性好,对于关系模型要求高的软件(例如需求固定的定制化软件)如果用hibernate开发可以节省很多代码,提高效率。但是Hibernate的学习门槛高,要精通门槛更高,而且怎么设计O/R映射,在性能和对象模型之间如何权衡,以及怎样用好Hibernate需要具有很强的经验和能力才行。
总之,按照用户的需求在有限的资源环境下只要能做出维护性、扩展性良好的软件架构都是好架构,所以框架只有适合才是最好。