Mybatis 是一个继承了传统的JDBC技术的dao层框架(持久层框架),可以使开发者只关注sql语句的本身而无需过多地考虑加载数据库驱动,创建连接,创建Statement 等复杂的过程。
Mybatis 通过xml或者注解的方式将要执行的statement配置起来,并通过java对象和statement中的sql的动态参数进行映射生成最终执行的sql语句,最后由Mybatis 框架将封装好的java对象返回。
采用ORM思想解决了实体和数据库的映射问题,对JDBC 进行了封装,屏蔽了jdbc api底层访问的细节。
前身是ibatis,关键是ORM(Object Relation Map)对象关系映射
注意:解决maven工程创建过慢一般可以在创建的时候添加: archetypeCatalog : internal
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0modelVersion>
<groupId>com.xxxgroupId>
<artifactId>mybatis_1artifactId>
<version>1.0-SNAPSHOTversion>
<packaging>jarpackaging>
<dependencies>
<dependency>
<groupId>org.mybatisgroupId>
<artifactId>mybatisartifactId>
<version>3.5.3version>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<version>5.1.48version>
dependency>
<dependency>
<groupId>log4jgroupId>
<artifactId>log4jartifactId>
<version>1.2.12version>
dependency>
<dependency>
<groupId>junitgroupId>
<artifactId>junitartifactId>
<version>4.12version>
<scope>testscope>
dependency>
dependencies>
project>
接着上边的搭建好的环境实现一个简单的案例,先创建两个新的包:
然后再domain包中创建一个实体类User,直接贴代码了:
private Integer id;
private String username;
private Date birthday;
private String sex;
private String address;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public Date getBirthday() {
return birthday;
}
public void setBirthday(Date birthday) {
this.birthday = birthday;
}
public String getSex() {
return sex;
}
public void setSex(String sex) {
this.sex = sex;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
@Override
public String toString() {
return "User{" +
"id=" + id +
", username='" + username + '\'' +
", birthday=" + birthday +
", sex='" + sex + '\'' +
", address='" + address + '\'' +
'}';
}
}
然后再在dao包创建一个UserDao接口:(后边就由mybatis来管理,并实现接口的功能了,不用写实现类)
public interface UserDao {
/**
* 查询所有的操作
*/
// @Select("select * from user") 这是使用注解,不使用xml映射文件配置的方式(这种方式不用写UserDao.xml)
List<User> findAll();
}
然后再在resources包下边新建一个SqlMapConfig.xml的住配置文件,用于存放搭建数据库连接等重要信息的基础环境的信息,和指定相应的映射文件,是一个mybatis全局环境配置包。详细的代码如下:
<configuration>
<environments default="mysql">
<environment id="mysql">
<transactionManager type="JDBC">transactionManager>
<dataSource type="POOLED">
<property name="driver" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/test_mybatis"/>
<property name="username" value="root"/>
<property name="password" value="root"/>
dataSource>
environment>
environments>
<mappers>
<mapper resource="com/xxx/dao/UserDao.xml"/>
mappers>
configuration>
然后在resources下继续新键包:com.xxx.dao,在此包里新建一个UserDao.xml(就是为了和上边的UserDao的结构都保持一致,必须一致才能正确执行!),UserDao.xml的详细代码:
<mapper namespace="com.xxx.dao.UserDao">
<select id="findAll" resultType="com.xxx.domain.User">
select * from user
select>
mapper>
至此,所有的包结构如图:
到这里完成的工作包括:
1.创建Maven工程,并导入相应的架包支持
2.创建一个实体类和一个实现查询的dao接口
3.创建一个住配置文件,存放环境配置的相关信息SqlMapConfig.xml
4.创建映射配置文件:UserDao.xml
开始创建一个测试类来测试:
先将log4j.properties导进resources下,然后在test测试包下新建一个包com.xxx.test,新建一个java文件:MyBatisTest.java:
public class MyBatisTest {
/**
* mybatis的入门案例
* @param args
*/
public static void main(String[] args) throws IOException {
//1.读取配置文件
InputStream in = Resources.getResourceAsStream("SqlMapConfig.xml");//读取配置文件的时候,一般使用类加载器或者ServletContext对象的getRealPath()
//2.创建SqlSessionFactory工厂
SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();//创建工厂的时候,mybatis的使用了”构建者模式“,使使用者直接调用方法即可拿到对象
SqlSessionFactory factory = builder.build(in);
//3.使用工厂生产SqlSession对象
SqlSession sqlSession = factory.openSession();//生产sqlSession使用了工厂模式,降低了类之间的依赖关系
//4.使用SqlSession创建Dao接口的代理对象
UserDao dao = sqlSession.getMapper(UserDao.class);//创建dao接口实现类使用了代理模式,不修改源码的基础上对已有方法增强
//5.使用代理对象执行方法
List<User> list = dao.findAll();
for(User user:list){
System.out.println(user);
}
//6.释放资源
sqlSession.close();
in.close();
}
}
运行成果:
最后的包结构:
总结:
mybatis基于注解的入门案例也在上边掺杂了,就是把UserDao.xml移除掉,在dao接口的方法上使用@Select注解,并且指定SQL语句,同时需要在SqlMapConfig.xml中的mapper配置时,使用class属性指定dao接口实现的全限定类名。
搭建环境时候需要注意:
1.mybatis中的mapper其实就是dao。
2.在idea中创建目录的时候需要注意(directory),他不同于包(package),目录想要和包一样的时候需要一级一级地创建的!
3.mybatis的映射文件位置必须和dao接口的包结构相同
4.映射配置文件的mapper标签namespace属性的取值必须是dao接口的全限定类名
5.映射配置文件的操作设置(select),id属性的取值必须是dao接口的方法名
当我们在开发中遵从了第三、四、五点之后,我们在开发中就无须再写dao的实现类。
//1.读取配置文件
InputStream in = Resources.getResourceAsStream("SqlMapConfig.xml");
//2.创建SqlSessionFactory工厂
SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
SqlSessionFactory factory = builder.build(in);
//3.使用工厂生产SqlSession对象
SqlSession session = factory.openSession();
//4.使用SqlSession创建Dao接口的代理对象
UserDao userDao = session.getMapper(UserDao.class);
//5.使用代理对象执行方法
List<User> users = userDao.findAll();
for(User user : users){
System.out.println(user);
}
//6.释放资源
session.close();
in.close();
其实就做了两件事:
最后形成的包结构:
此MyBatis的简单实现案例中包含了大量的优秀的设计模式思想,值得借鉴!
写一个类似上边的案例
在UserDao.xml中新增:
在userDao.java中新增:
void saveUser(User user);
在Userdao.xml中新增
<insert id="saveUser" parameterType="com.zhou.domain.User">
insert into user(username,address,sex,birthday) values(#{username},#{address},#{sex},#{birthday});
</insert>
在测试中新增:
User user = new User();
user.setUsername("mybatis_test");
user.setAddress("杭州");
user.setSex("男");
user.setBirthday(new Date());
//执行保存方法
userDao.saveUser(user);//user对象必须传进来
//提交事务
sqlSession.commit();
所有方法的代码:
UserDao.java:
public interface UserDao {
/**
* 查询所有的方法
*/
List<User> findAll();
/**
* 创建一个用于保存用户信息的方法
*/
void saveUser(User user);
/**
* 创建一个方法用于更新用户操作
*/
void updateUser(User user);
/**
* 创建一个方法用于删除操作
*/
void deleteUser(Integer id);
/**
* 创建查询一个用户操作
*/
User findById(Integer id);
/**
* 创建根据名字查询
*/
List<User> findByName(String name);
/**
* 创建一个方法查询总记录条数
*/
int findTotal();
}
UserDao.xml:
<?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="com.zhou.dao.UserDao">
<select id="findAll" resultType="com.zhou.domain.User">
select * from user;
</select>
<insert id="saveUser" parameterType="com.zhou.domain.User">
<!-- 保存操作中,可以返回id的属性值,或者其他的属性值,下边的语句表示在执行插入操作之后返回一个int类型的id值
其中第一个keyProperty表示的是id的属性值,第二个keyColumn表示的是插入到数据库的哪一列-->
<selectKey keyProperty="id" keyColumn="id" resultType="int" order="AFTER">
select last_insert_id();
</selectKey>
insert into user(username,address,sex,birthday) values(#{username},#{address},#{sex},#{birthday});
</insert>
<update id="updateUser" parameterType="com.zhou.domain.User">
update user set username=#{username},address=#{address},sex=#{sex},birthday=#{birthday}
where id=#{id};
</update>
<!--注意这里的uid只是一个占位符,随便写个aa之类也是可以的-->
<delete id="deleteUser" parameterType="Integer">
delete from user where id=#{uid};
</delete>
<select id="findById" parameterType="int" resultType="com.zhou.domain.User">
select * from user where id=#{uid};
</select>
<select id="findByName" parameterType="String" resultType="com.zhou.domain.User">
<!-- select * from user where username like '%${value}%'; -->
select * from user where username like #{username};
</select>
<select id="findTotal" resultType="int">
select count(*) from user;
</select>
</mapper>
</mapper>
测试类:
public class MyTest {
private InputStream resourceAsStream;
private SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
private SqlSessionFactory factory;
private SqlSession sqlSession;
private UserDao userDao;
@Before //测试之前执行初始化工作
public void init() throws IOException {
resourceAsStream = Resources.getResourceAsStream("SqlMapConfig.xml");
factory = builder.build(resourceAsStream);
sqlSession = factory.openSession();
userDao = sqlSession.getMapper(UserDao.class);
}
@After //测试完了之后提交事务,释放资源
public void release() throws IOException {
//提交事务
sqlSession.commit();
sqlSession.close();
resourceAsStream.close();
}
/**
* 测试查询所有操作
*/
@Test
public void test(){
List<User> users = userDao.findAll();
for(User user : users){
System.out.println(user);
}
}
/**
* 测试保存一个用户的操作
*/
@Test
public void testSaveUser(){
User user = new User();
user.setUsername("奥术飞弹");
user.setAddress("新疆");
user.setSex("男");
user.setBirthday(new Date());
System.out.println("执行保存操作之前:"+user);
//执行保存方法
userDao.saveUser(user);//user对象必须传进来
System.out.println("执行保存操作之后:"+user);
}
@Test
public void updateUser(){
User user = new User();
user.setId(45);
user.setUsername("不是人");
user.setAddress("广州");
user.setSex("女");
user.setBirthday(new Date());
userDao.updateUser(user);
}
@Test
public void deleteUser(){
userDao.deleteUser(41);
}
@Test
public void selectById(){
User user = userDao.findById(48);
System.out.println(user);
}
@Test
public void findByName(){
List<User> users = userDao.findByName("%人%");
for(User user : users){
System.out.println(user);
}
}
@Test
public void findTotal(){
int total = userDao.findTotal();
System.out.println(total);
}
}
1.MyBatis可以传进去的参数(parameterType)可以使基本的数据类型,也可以是pojo对象,类似于上边例子中的User,还可以是OGNL表达式,即对象图导航语言表达式,user.getUserName()在OGNL 中可以写成user.username,例如在添加一个根据对象查询的时候可以这样做:
在domain包中增加一个QueryVo:
public class QueryVo {
private User user;
public User getUser() {
return user;
}
public void setUser(User user) {
this.user = user;
}
}
在UserDao.xml中添加方法:
/**
* 创建一个根据查询对象去查询的方法
*/
List<User> findByVo(QueryVo vo);
在UserDao.xml添加查询语句(根据OGNL表达式查询):
<!-- 根据查询对象去查询,下边是用的就是OGNL表达式 -->
<select id="findByVo" parameterType="com.zhou.domain.QueryVo" resultType="com.zhou.domain.User">
select * from user where username like #{user.username};
</select>
测试:
@Test
public void findByVo(){
QueryVo vo = new QueryVo();
User user = new User();
user.setUsername("%人%");
vo.setUser(user);
List<User> users = userDao.findByVo(vo);
for(User u:users)
System.out.println(u);
}
2.MyBatis中的结果类型参数可以是一个基本类型,也可以是一个pojo(实体类)对象,或者pojo 列表
其实,所有的CUD(增删改)方法使用的都是update,而R(查询)使用的是selectList,最后执行的jdbc语句都是PrepareStatement中的execute()方法。execute()返回的都是boolean值,表示有没有结果集封装返回。
下边代码清晰表明了CUD的实质就是一个update:
@Override
public int insert(String statement) {
return insert(statement, null);
}
@Override
public int insert(String statement, Object parameter) {
return update(statement, parameter);
}
@Override
public int update(String statement) {
return update(statement, null);
}
@Override
public int update(String statement, Object parameter) {
try {
dirty = true;
MappedStatement ms = configuration.getMappedStatement(statement);
//在所有的cud操作中,executor.update方法是最重要的,他会到达CashingExecutor中的update方法
return executor.update(ms, wrapCollection(parameter));
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error updating database. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
@Override
public int delete(String statement) {
return update(statement, null);
}
@Override
public int delete(String statement, Object parameter) {
return update(statement, parameter);
}
CashingExecutor中的update方法:
@Override
public int update(MappedStatement ms, Object parameterObject) throws SQLException {
flushCacheIfRequired(ms);
return delegate.update(ms, parameterObject);
}
然后执行delegate.update(),此方法是Executor接口中的中的一个方法,由子类(抽象类)BaseExecutor实现:
Executor抽象类中的update()方法:
public interface Executor {
ResultHandler NO_RESULT_HANDLER = null;
int update(MappedStatement ms, Object parameter) throws SQLException;
抽象类BaseExecutor中的update方法:
@Override
public int update(MappedStatement ms, Object parameter) throws SQLException {
ErrorContext.instance().resource(ms.getResource()).activity("executing an update").object(ms.getId());
if (closed) {
throw new ExecutorException("Executor was closed.");
}
clearLocalCache();
return doUpdate(ms, parameter);
}
此时返回的doUpdate()方法其实是个抽象方法:
protected abstract int doUpdate(MappedStatement ms, Object parameter)
throws SQLException;
doUpdate()方法最后由继承抽象类BaseExecuto的 SimpleExecutor 来进行实现:
@Override
public int doUpdate(MappedStatement ms, Object parameter) throws SQLException {
Statement stmt = null;
try {
Configuration configuration = ms.getConfiguration();
StatementHandler handler = configuration.newStatementHandler(this, ms, parameter, RowBounds.DEFAULT, null, null);
stmt = prepareStatement(handler, ms.getStatementLog());
return handler.update(stmt);
} finally {
closeStatement(stmt);
}
}
这里返回的handler.update(stmt),其实是调用了接口StatementHandler中的抽象方法update(Statement statement):
int update(Statement statement)
throws SQLException;
而这个方法由继承StatementHandler接口的实现类PreparedStatementHandler实现:
@Override
public int update(Statement statement) throws SQLException {
PreparedStatement ps = (PreparedStatement) statement;
ps.execute();
int rows = ps.getUpdateCount();
Object parameterObject = boundSql.getParameterObject();
KeyGenerator keyGenerator = mappedStatement.getKeyGenerator();
keyGenerator.processAfter(executor, mappedStatement, ps, parameterObject);
return rows;
}
到此,MyBatis调用的jdbc语句终于出现了,就是使用了PreparedStatement 的execute()方法来进行操作的。
而查询方法R,无论是带参数查询还是不带参数查询,执行的最后都是同一个方法,下边的代码为证:
@Override
public <E> List<E> selectList(String statement) {
return this.selectList(statement, null);
}
@Override
public <E> List<E> selectList(String statement, Object parameter) {
return this.selectList(statement, parameter, RowBounds.DEFAULT);
}
@Override
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
try {
MappedStatement ms = configuration.getMappedStatement(statement);
return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
使用mybatis之所以不用编写dao的实现类,关键在于它使用了代理的方式,获取getMapper,而在使用getMapper的时候其实最后调用的也是selectList等方法,然后执行的也是上边的过程,都是一样的执行过程。
重点是properties以及package的使用:
<!-- mybatis 的主配置文件-->
<configuration>
<!-- 配置properties
可以在标签内部配置连接数据库的信息,也可以通过属性引用外部配置文件的信息
resource属性:用于指定配置文件的位置,是按照路径的写法来写的,并且需要在类路径下
url属性:是要求按照Url的写法来填写地址
URL:Uniform Resource Locator 统一资源定位符,他是可以唯一表示一个资源的位置的
他的写法:
http://localhost:8080/mybatisServer/demo1
协议 主机 端口 URI
URI:Uniform Resource Identifier 统一资源定位标识符。他是应用中可以唯一定位一个资源的。
-->
<!-- <properties resource="jdbcConfig.properties">-->
<!-- 使用url定位的话需要写完整的协议等信息 -->
<properties url="file:///E:/Code/day02_mybatis_2/src/main/resources/jdbcConfig.properties">
<!-- <property name="driver" value="com.mysql.jdbc.Driver"></property>-->
<!-- <property name="url" value="jdbc:mysql://localhost:3306/test_mybatis"></property>-->
<!-- <property name="username" value="root"></property>-->
<!-- <property name="password" value="root"></property>-->
</properties>
<!-- 使用typeAliases配置别名,他只能配置domain中类的别名 -->
<typeAliases>
<!-- typeAlias用于配置别名,type属性制定的是实体类的全限定名,alias属性指定别名,当制定了别名,就不再区分大小写了 -->
<!-- <typeAlias type="com.zhou.domain.User" alias="user"></typeAlias>-->
<!--下边的这个用于指定配置别名的包,当置顶了之后,该报下的实体类都会被注册别名,并且类名就是别名,不区分大小写-->
<package name="com.zhou.domain"/>
</typeAliases>
<!-- 配置环境-->
<environments default="mysql">
<!--配置mysql的环境,这里的id需要和上边的定义的默认值保持一致-->
<environment id="mysql">
<!-- 配置事务类型-->
<transactionManager type="JDBC"></transactionManager>
<!-- 配置数据源(连接池)-->
<dataSource type="POOLED">
<!-- 配置数据库的四个基本信息,驱动类型,url,username,password-->
<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>
<!-- 指定映射文件的配置,映射配置文件指的是每个dao独立的配置文件 com/xxx/dao/UserDao.xml
如果使用注解的方式来配置的话,此处应该使用class属性指定北朱解的dao的全限定名 com.xxx.dao.UserDao-->
<mappers>
<!-- <mapper resource="com/zhou/dao/UserDao.xml"/>-->
<!-- package标签适用于指定dao接口所在包,当指定了之后就不需要写mapper以及resource或者class -->
<package name="com.zhou.dao"></package>
</mappers>
</configuration>
与多表查询合在一起来写了。
我们在实际开发中都会使用到连接池,因为它可以减少我们获取连接的时间(用到的是一种队列数据结构的思想,即先进先出)。
mybatis中的连接池提供了三种方式的配置:type属性就是采用何种连接池的方式,type的取值包括:POOLED:采用传统的javax.sql.DataSource规范中的连接池
UNPOOLED:采用传统的获取连接的方式,虽然也实现了javax.sql.DataSource接口,但是并没有使用池
JNDI:采用服务器提供的JNDI技术实现,来获取DataSource对象,不同的服务器所能获得的是不一样的。注意不是web或者war工程是不能使用的。而Tomcat服务器使用的是dbcp连接池。
关于事务,面试问题一般会涉及:1.什么识事务,2.事务的四大特性,3.不考虑隔离会产生的3个问题,4.解决方法:四种隔离的级别
:抽取重复的sql语句
:使用标签内得sql语句
:条件判断语句
:就是where的查询,可以包裹
:循环遍历语句
标签使用实例:
<mapper namespace="com.zhou.dao.UserDao">
<resultMap id="userMap" type="com.zhou.domain.User">
<result property="id" column="id">result>
<result property="username" column="username">result>
<result property="birthday" column="birthday">result>
<result property="sex" column="sex">result>
<result property="address" column="address">result>
resultMap>
<sql id="findUser">
select * from user
sql>
<select id="findAll" resultType="com.zhou.domain.User">
<include refid="findUser">include>
select>
<select id="findByUserCondition" resultMap="userMap" parameterType="user">
select * from user
<where>
<if test="username != null">
and username like #{username}
if>
<if test="sex != null">
and sex = #{sex}
if>
where>
select>
<select id="findUserInIds" resultMap="userMap" parameterType="queryVo">
<include refid="findUser">include>
<if test="ids != null and ids.size()>0">
<foreach collection="ids" open="where id in(" close=")" item="id" separator=",">
#{id}
foreach>
if>
select>
mapper>
用户与订单的关系(一对多,多对一):
一对多:比如一个用户可以对应多个订单
多对一:比如多个订单可以对应一个用户
人和身份证(一对一):一个人只有一个身份证号,一个身份证号只能对应一个人
大学老师和学生的关系(多对多):一个老师可以教多个学生,一个学生也可以上多个老师的课程,并允许交叉
但是也有特殊的,比如说一个订单对应的只能是一个用户,所以上边分析的一对多其实很多时候就是一对一的一种特例,mybatis 就是把一对多或者多对一看成了一对一的。
步骤:
1.建立一个用户表和一个账户表,一个用户可以有多个账户,一个账户只能属于一个用户(多个账户可能同属于一个用户),所以在账户表(多的一方)添加外键。
2.创建两个实体类,同时创建两个查询的xml
3.实现的功能:查询一个用户的时候可以查询到它对应的所有的账户信息,查询一个账户的时候,可以查询到它对应的用户的信息。
4.用户与角色的关系
用户可以有多个角色。一个角色可以赋予多个用户。使用中间表表示多对多的关系。
缓存就是存在于内存里的临时数据。使用缓存的原因:减少和数据库交互的次数,提高效率。
经常查询并且不经常改变的数据可以使用缓存。数据正与否对最终的结果影响不大的也可以使用缓存。
经常改变的数据,数据的正确与否对最终的结果影响很大的情况下都不能使用缓存。例如商品库存,银行汇率,股市牌价
Mybatis的缓存:
一级缓存:指的是MyBatis中的SqlSession对象的缓存。当我们执行查询之后,查询的结果会同时存入到SqlSession为我们提供的一块区域中,该区域的结构是一个Map,当我们再次发起查询同样的数据的时候,mybatis回西安区sqlsession中去查询是否有,有的话直接拿出来使用。当SqlSession对象小时的时候,以及缓存也会跟着消失。
二级缓存:指的是MyBatis中的SqlSessionFactory对象的缓存。由同一个SqlSessionFactory对象创建。
二级缓存使用步骤:
1.延迟加载:在真正使用数据的时候才发起查询,不用的时候不查询,按需查询(懒加载)。
例如在查询用户的时候,用户下的账户信息是什么时候使用,什么时候查询出来?(可能一个用户有100多个账户,立即加载出来得时候会极大地浪费内存空间)
2.立即加载:不管用不用,只要发起查询就马上发起查询。例如,查询一个账户对应的用户信息。
一般来说,一对多,多对多中都是用延迟加载;而多对一,或者一对一中就使用立即加载。
Mybatis中一旦使用了注解开发就不能再使用xml文件的了。