网站首页 > 技术文章 正文
声明:内容来源于互联网,笔者主要进行了相关整理。
一、问题分析
1.jdbc数据库访问demo
public static void main(String[] args){
Connection connection = null;
PreparedStatement preparedStatement = null;
ResultSet resultSet = null;
try{
// 加载数据库驱动
Class.forName("com.mysql.jdbc.Driver");
// 通过驱动管理数据类获取数据库连接
connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/mybatis","root","111111");
// 定义sql语句 ?表示占位符
String sql = "select * from user where username = ?";
// 预编译sql
preparedStatement = connection.prepareStatement(sql);
// 设置参数,第一个参数为sql语句中参数的序号(从1开始),第二个参数为设置的参数的值
preparedStatement.setString(1,"zhangsan");
// 向数据库发送sql执行查询,查询出结果集
resultSet = preparedStatement.executeQuery();
// 遍历结果集
User user = new User();
while(resultSet.next()){
int id = resultSet.getInt("id");
String username = resultSet.getString("username");
// 封装user对象
user.setId(id);
user.setUsername(username);
}
System.out.println(user);
}catch(Exception e){
e.printStackTrace();
}finally {
// 释放连接
try {
connection.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
2.JDBC访问数据问题分析:
1.数据库配置信息存在硬编码--解决方案:配置文件
2.频繁创建释放数据库连接--解决方案:连接池
3.sql语句、设置参数、获取结果集参数均存在硬编码问题--解决方案:配置文件
4.手动封装返回结果集,较为繁琐。--解决方案:反射
二、自定义持久层框架设计思想:
1、使用端(项目):引入自定义持久层框架的jar包
提供两部分配置信息:数据库配置信息,sql配置信息:sql语句,参数类型,返回值类型
使用配置文件来提供这两部分信息
1)sqlMapConfig.xml:存储数据库配置信息,存放mapper.xml的全路径
2)mapping.xml:存放sql配置信息
2、自定义持久层框架(工具):本质就是对jdbc代码进行封装
1)加载配置文件:根据配置文件的路径,加载配置文件成字节输入流,存储在内存中
创建Resources类,方法InputStream getResourceAsStream(String path)
2)创建容器对象:两个JavaBean,存放的就是配置文件解析出来的容器对象
Configuration:核心配置类,存放sqlMapConfig.xml解析出来的内容
MappedStatement:映射配置类,存放mapping.xml解析出来的内容
3)解析配置配置文件:dom4j
创建类:SqlSessionFactoryBuilder 方法:build(InputStream in)
第一:使用dom4j解析配置文件,将解析出来的内容封装到容器对象中
第二:创建SqlSessionFactory对象:生产sqlSession会话对象(工厂模式)
4)创建SqlSessionFactory接口及实现类DefaultSqlSessionFactory
第一:openSession()生产sqlSession
5)创建SqlSession接口及实现类DefaultSession
定义数据库的curd操作:selectList(),selectOne(),update(),delete()
6)创建Executor接口及实现SimpleExecutor实现类
query(Configuration,MappedStatement,Object...params)执行的就是jdbc代码
三、实现过程
1.编写sqlMapConfig.xml
用于存放数据库连接配置,并关联mapper
<configuration>
<!--数据库配置信息-->
<dataSource>
<property name="driverClass" value="com.jdbc.Driver"></property>
<!-- ///表示本地的数据 -->
<property name="jdbcUrl" value="jdbc:mysql:///mybatis"></property>
<property name="username" value="root"></property>
<property name="password" value="111111"></property>
</dataSource>
<!--存放mapper.xml的全路径-->
<mapper resource="UserMapper.xml"></mapper>
</configuration>
2.编写sql配置信息
<mapper namespace="com.mybatis.dao.IUserDao">
<!--
select表示查询
id表示sql的标识
sql的唯一标识:namespace.id来组成,statementId
resultType返回值类型
-->
<select id="findAll" resultType="com.mybatis.pojo.User">
select * from user
</select>
<!--
参数传递:
User user = new User();
user.setId(1);
user.setUsername("tom")
使用#{xx}通过反射的方式,从paramType中获取xx属性的值作为sql的参数
-->
<select id="findByCondition" resultType="com.mybatis.pojo.User" paramType="com.mybatis.pojo.User">
select * from user where id = #{id} and username = #{username}
</select>
</mapper>
3.配置文件为输入流
public class Resources {
/**
* 根据配置文件的路径,将配置文件加载成字节输入流,存储在内存中
* @param path
* @return
*/
public static InputStream getResourcesAsStream(String path){
InputStream resourceAsStream = Resources.class.getClassLoader().getResourceAsStream(path);
return resourceAsStream;
}
}
4.编写配置文件解析实体
1)mappedStatement解析mapper文件内容
public class MappedStatement {
/**
* id标识
*/
private String id;
/**
* 返回值类型
*/
private String resultType;
/**
* 参数类型
*/
private String paramType;
/**
* sql语句
*/
private String sql;
2)configuration对sqlMapConfig解析
public class Configuration {
/**
* 数据库配置信息
*/
private DataSource dateSource;
/**
* key:statementId就是sql的唯一标识,namespace+sql id
* value:一个封装好sql数据
*/
private Map<String,MappedStatement> mappedStatementMap = new HashMap();
5.获取SqlSessionFactory对象
1)解析sqlMapConfig.xml配置信息封装成Configuration
public class XMLConfigBuilder {
private Configuration configuration;
public XMLConfigBuilder(){
this.configuration = new Configuration();
}
/**
* 将配置文件解析成并封装成Configuration
* @param in
* @return
*/
public Configuration parseConfig(InputStream in) throws DocumentException, PropertyVetoException {
Document document = new SAXReader().read(in);
// 获取到跟<configuration>
Element configurationElt = document.getRootElement();
// 获取dataSource的配置,并使用连接池对象
List<Element> list = configurationElt.selectNodes("//property");
Properties properties = new Properties();
for(Element element:list){
String name = element.attributeValue("name");
String value = element.attributeValue("value");
properties.put(name,value);
}
// 封装成连接池对象
ComboPooledDataSource comboPooledDataSource = new ComboPooledDataSource();
comboPooledDataSource.setDriverClass(properties.getProperty("driverClass"));
comboPooledDataSource.setJdbcUrl(properties.getProperty("jdbcUrl"));
comboPooledDataSource.setUser(properties.getProperty("username"));
comboPooledDataSource.setPassword(properties.getProperty("password"));
configuration.setDateSource(comboPooledDataSource);
// mapper.xml解析:拿到路径--字节输入流--dom4j解析
List<Element> mapperList = configurationElt.selectNodes("//mapper");
for(Element element:mapperList){
String mapperPath = element.attributeValue("resource");
InputStream mapperIn = Resources.getResourcesAsStream(mapperPath);
XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(configuration);
xmlMapperBuilder.parse(mapperIn);
}
return configuration;
}
}
2)mapper配置文件信息解析
public class XMLMapperBuilder {
private Configuration configuration;
public XMLMapperBuilder(Configuration configuration){
this.configuration = configuration;
}
/**
* 解析mapper.xml
* @param in
* @throws DocumentException
*/
public void parse(InputStream in) throws DocumentException {
Document document = new SAXReader().read(in);
Element rootElt = document.getRootElement();
String namespace = rootElt.attributeValue("namespace");
/**
* <select id="selectOne" resultType="com.persistence.pojo.User" paramType="com.persistence.pojo.User">
* select * from user where id = #{id} and username = #{username}
* </select>
*/
List<Element> elementList = rootElt.selectNodes("//select");
for(Element element:elementList){
String id = element.attributeValue("id");
String resultType = element.attributeValue("resultType");
String paramType = element.attributeValue("paramType");
String sql = element.getTextTrim();
MappedStatement mappedStatement = new MappedStatement();
mappedStatement.setId(id);
mappedStatement.setResultType(resultType);
mappedStatement.setParamType(paramType);
mappedStatement.setSql(sql);
String statementId = namespace+"."+id;
configuration.getMappedStatementMap().put(statementId,mappedStatement);
}
}
}
3)sqlSession工厂
//sqlSession接口
public interface SqlSessionFactory {
}
//sqlSession默认实现
public class DefaultSessionFactory implements SqlSessionFactory {
private Configuration configuration;
public DefaultSessionFactory(Configuration configuration){
this.configuration = configuration;
}
}
4)SqlSession工厂构造流程化
// 通过sqlSessionFactoryBuilder来串联整个步骤
public class SqlSessionFactoryBuilder {
/**
* 第一:使用dom4j解析配置文件,将解析出来的内容封装到Configuration中
* 第二:创建sqlSessionFactory对象
* @param in
* @return
*/
public SqlSessionFactory build(InputStream in) throws DocumentException, PropertyVetoException {
//第一:使用dom4j解析配置文件,将解析出来的内容封装到Configuration中
XMLConfigBuilder xmlConfigBuilder = new XMLConfigBuilder();
Configuration configuration = xmlConfigBuilder.parseConfig(in);
//第二:创建sqlSessionFactory对象,工厂类生产sqlSession会话对象
SqlSessionFactory sqlSessionFactory = new DefaultSessionFactory(configuration);
return sqlSessionFactory;
}
}
6.获取SqlSession,并执行sql
1)从sqlSession中获取sqlSession
// sqlSession工厂接口定义获取SqlSession的方法
public interface SqlSessionFactory {
SqlSession openSession();
}
//sqlSesssion工厂中实现获取sqlSession的方法
public class DefaultSessionFactory implements SqlSessionFactory {
private Configuration configuration;
public DefaultSessionFactory(Configuration configuration){
this.configuration = configuration;
}
@Override
public SqlSession openSession() {
return new DefaultSqlSession(configuration);
}
}
2)sqlSession中调用执行器,执行具体的方法
// sqlSession接口定义常用sql方法(curd)
public interface SqlSession {
/**
* 查询多个对象
* @param statementId
* @param param
* @param <E>
* @return
*/
<E> List<E> selectList(String statementId,Object...param);
/**
* 查询一个对象
* @param <E>
* @param statementId
* @param param
*/
<E> E selectOne(String statementId, Object...param);
}
//DefaultSqlSession实现中实现方法
public class DefaultSqlSession implements SqlSession {
private Configuration configuration;
public DefaultSqlSession(Configuration configuration) {
this.configuration = configuration;
}
@Override
public <E> List<E> selectList(String statementId, Object... param) {
// 构建sql执行器
Executor executor = new SimpleExecutor();
// 获取mapper配置中所有sql配置信息
Map<String, MappedStatement> mappedStatementMap = configuration.getMappedStatementMap();
try {
// 执行当前sql
return executor.queryList(configuration,mappedStatementMap.get(statementId),param);
} catch (SQLException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IntrospectionException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
return null;
}
@Override
public <E> E selectOne(String statementId, Object... param) {
List<Object> objects = selectList(statementId,param);
if(objects.size()==1){
return (E)objects.get(0);
}else{
throw new RuntimeException("查询结果为空或者返回结果为多个");
}
}
3.Executor执行器
//exector执行器接口定义
public interface Executor {
<E> List<E> queryList(Configuration configuration, MappedStatement mappedStatement,Object...params) throws SQLException, ClassNotFoundException, NoSuchFieldException, IllegalAccessException, InstantiationException, IntrospectionException, InvocationTargetException;
}
//执行器实现
public class SimpleExecutor implements Executor{
@Override
public <E> List<E> queryList(Configuration configuration, MappedStatement mappedStatement,Object...params) throws SQLException, ClassNotFoundException, NoSuchFieldException, IllegalAccessException, InstantiationException, IntrospectionException, InvocationTargetException {
//1.注册驱动,活动连接
Connection connection = configuration.getDateSource().getConnection();
//2.获取sql select * from user where id = #{id} and username = #{username}
// 转换为jdbc的 select * from user where id = ? and username = ?
String sql = mappedStatement.getSql();
BoundSql boundSql = getBoundSql(sql);
//3.预处理对象preparedStatement
PreparedStatement preparedStatement = connection.prepareStatement(boundSql.getSqlText());
//4.设置参数
//获取参数类型
String paramType = mappedStatement.getParamType();
Class<?> paramTypeClass = getClassType(paramType);
List<ParameterMapping> parameterMappingList = boundSql.getParameterMappingList();
for(int i=0;i<parameterMappingList.size();i++){
ParameterMapping parameterMapping = parameterMappingList.get(i);
//获取sql参数中属性值(#{}中的值)
String content = parameterMapping.getContent();
//使用反射,根据返回获取content属性对应参数中的值
Field declaredField = paramTypeClass.getDeclaredField(content);
//暴力访问
declaredField.setAccessible(true);
Object o = declaredField.get(params[0]);
preparedStatement.setObject(i+1,o);
}
//5.执行sql
ResultSet resultSet = preparedStatement.executeQuery();
//6.封装返回结果集
String resultType = mappedStatement.getResultType();
Class<?> resultTypeClass = getClassType(resultType);
List<Object> objects = new ArrayList<Object>();
while(resultSet.next()){
Object o = resultTypeClass.newInstance();
ResultSetMetaData metaData = resultSet.getMetaData();
for(int i=0;i<metaData.getColumnCount();i++){
//字段名
String columnName = metaData.getColumnName(i+1);
//字段值
Object value = resultSet.getObject(columnName);
//使用反射,根据数据库表和实体的对应关系,完成封装
PropertyDescriptor propertyDescriptor = new PropertyDescriptor(columnName,resultTypeClass);
Method method = propertyDescriptor.getWriteMethod();
method.invoke(o,value);
}
objects.add(o);
}
return (List<E>)objects;
}
private Class<?> getClassType(String paramType) throws ClassNotFoundException {
if(paramType!=null){
Class<?> aClass = Class.forName(paramType);
return aClass;
}
return null;
}
/**
* 完成对#{}的解析工作,1.将#{}使用?进行代替,2.解析出#{}里面的值进行存储
* @param sql
* @return
*/
private BoundSql getBoundSql(String sql){
// 标记处理类:配置标记解析器完成对占位符的解析处理工作
ParameterMappingTokenHandler parameterMappingTokenHandler = new ParameterMappingTokenHandler();
GenericTokenParser genericTokenParser = new GenericTokenParser("#{","}",parameterMappingTokenHandler);
//解析过后的sql
String parseSql = genericTokenParser.parse(sql);
//#{}解析出来的参数名称
List<ParameterMapping> parameterMappingList = parameterMappingTokenHandler.getParameterMappingList();
BoundSql boundSql = new BoundSql(parseSql,parameterMappingList);
return boundSql;
}
}
4)sql占位符分析
//参数中属性值#{}中的具体值
public class ParameterMapping {
private String content;
public ParameterMapping(String content) {
this.content = content;
}
public String getContent() {
return content;
}
}
//占位符解析器定义
public interface TokenHandler {
String handleToken(String content);
}
public class ParameterMappingTokenHandler implements TokenHandler{
private List<ParameterMapping> parameterMappingList = new ArrayList();
@Override
public String handleToken(String content) {
parameterMappingList.add(buildParameterMapping(content));
return "?";
}
private ParameterMapping buildParameterMapping(String content) {
ParameterMapping parameterMapping = new ParameterMapping(content);
return parameterMapping;
}
public List<ParameterMapping> getParameterMappingList() {
return parameterMappingList;
}
}
public class GenericTokenParser{
/**
* 开始标记
*/
private String openToken;
/**
* 结束标记
*/
private String closeToken;
/**
* 标记处理器
*/
private TokenHandler handler;
public GenericTokenParser(String openToken, String closeToken, TokenHandler handler) {
this.openToken = openToken;
this.closeToken = closeToken;
this.handler = handler;
}
/**
* 解析#{}和${}
* @param text
* @return
* 该方法主要实现了配置文件、脚本等片段中占位符的解析、处理工作,并返回最终需要的数据。
* 其中,解析工作由该方法完成,处理工作是由处理器handler的handlerToken()方法完成
*/
public String parse(String text) {
// 验证参数问题,如果是null,就返回空字符串
if(text==null||text.isEmpty()){
return "";
}
// 下面继续验证是否包含开始标记和结束标记,默认不是占位符,直接原样返回即可,否则继续执行
int start = text.indexOf(openToken,0);
if(start==-1){
return text;
}
//把text转成字符串数组src,并且定义默认偏移量offset=0,存储最终需要返回的字符串的变量builder
//text变量中占位符对应的变量名expression。判断start是否大于-1(即text中是否存在openToken),如果存在则执行如下代码
char[] src = text.toCharArray();
int offset = 0;
final StringBuilder builder = new StringBuilder();
StringBuilder expression = null;
while(start>-1){
//判断如果开始标记前如果有转义字符,就不作为openToken进行处理,否则继续处理
if(start>0&&src[start-1]=='\\'){
builder.append(src,offset,start-offset-1).append(openToken);
offset = start+openToken.length();
}else{
//重置expression变量,避免空指针或者老数据干扰
if(expression==null){
expression = new StringBuilder();
}else{
expression.setLength(0);
}
builder.append(src,offset,start-offset);
offset = start+openToken.length();
int end = text.indexOf(closeToken,offset);
while(end>-1){
//存在结束标记
if(end>offset&&src[end-1]=='\\'){
expression.append(src,offset,end-offset-1).append(closeToken);
}else{
expression.append(src,offset,end-offset);
offset=end+closeToken.length();
break;
}
}
if(end==-1){
builder.append(src,start,src.length-start);
offset=src.length;
}else{
builder.append(handler.handleToken(expression.toString()));
offset=end+closeToken.length();
}
}
start=text.indexOf(openToken,offset);
}
return builder.toString();
}
}
public class BoundSql {
/**
* 解析后的sql
*/
private String sqlText;
/**
* 参数
*/
private List<ParameterMapping> parameterMappingList;
public BoundSql(String sqlText, List<ParameterMapping> parameterMappingList) {
this.sqlText = sqlText;
this.parameterMappingList = parameterMappingList;
}
public String getSqlText() {
return sqlText;
}
public List<ParameterMapping> getParameterMappingList() {
return parameterMappingList;
}
}
5)测试流程
@Test
public void test() throws Exception {
InputStream in = Resources.getResourcesAsStream("sqlMapConfig.xml");
SqlSessionFactory sessionFactory = new SqlSessionFactoryBuilder().build(in);
SqlSession sqlSession = sessionFactory.openSession();
// 流程测试
User user = new User();
user.setId(1);
user.setUsername("zhangsan");
User user0 = sqlSession.selectOne("com.mybatis.dao.IUserDao.findByCondition",user);
System.out.println(user0);
}
6)编写dao和dao实现
public interface IUserDao {
List<User> findAll() throws Exception;
User findByCondition(User user) throws Exception;
}
public class UserDaoImpl implements IUserDao{
public List<User> findAll() throws Exception{
InputStream in = Resources.getResourcesAsStream("sqlMapConfig.xml");
SqlSessionFactory sessionFactory = new SqlSessionFactoryBuilder().build(in);
SqlSession sqlSession = sessionFactory.openSession();
// 调用
List<User> userList = sqlSession.selectList("com.mybatis.dao.IUserDao.findAll",null);
for(User user:userList){
System.out.println(user);
}
return userList;
}
public User findByCondition(User user) throws Exception{
InputStream in = Resources.getResourcesAsStream("sqlMapConfig.xml");
SqlSessionFactory sessionFactory = new SqlSessionFactoryBuilder().build(in);
SqlSession sqlSession = sessionFactory.openSession();
// 调用
User user2 = sqlSession.selectOne("com.mybatis.dao.IUserDao.findByCondition",user);
System.out.println(user2);
return user2;
}
}
测试流程
@Test
public void test() throws Exception {
InputStream in = Resources.getResourcesAsStream("sqlMapConfig.xml");
SqlSessionFactory sessionFactory = new SqlSessionFactoryBuilder().build(in);
SqlSession sqlSession = sessionFactory.openSession();
// 流程测试
User user = new User();
user.setId(1);
user.setUsername("zhangsan");
User user0 = sqlSession.selectOne("com.mybatis.dao.IUserDao.findByCondition",user);
System.out.println(user0);
// 传统方式调用(需要在dao每个方法中获取sqlSession,重复代码比较多)
IUserDao iUserDao = new UserDaoImpl();
User user1 = iUserDao.findByCondition(user);
System.out.println(user1);
}
此时发现,自定义持久层框架问题分析:
1.dao层使用自定义持久层框架,存在代码重复,整个操作的过程模板重复(加载配置文件,创建sqlSessionFactory,sqlSession).
2.statementId存在硬编码
解决方案:
使用代理模式生成dao层接口代理实现类
7)编写代理模式getMapper
public interface SqlSession {
/**
* 为dao接口生成代理实现类
* @param <T>
* @return
*/
<T> T getMappper(Class<?> mapperClass);
}
@Override
public <T> T getMappper(Class<?> mapperClass) {
// 使用jdk动态代理来为dao接口生成代理对象,并返回。
Object proxyInstance = Proxy.newProxyInstance(DefaultSqlSession.class.getClassLoader(), new Class[]{mapperClass}, new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// proxy:当前代理对象的应用
// method:当前被调用方法的引用
// args:传递的参数
//底层还是去执行jdbc代码,根据不同情况,来执行selectOne或者selectList
//准备参数:
//参数1:statementId:sql语句的唯一表示,由mapper文件的namespace.id组成。
// 此时无法获取到,但是通常会按照一定规范来操作,即namespace值使用dao的全限定名,id使用方法名
String methodName = method.getName();
String className = method.getDeclaringClass().getName();
String statementId = className+"."+methodName;
//参数2:就是args
// 获取调用方法执行调用
Type genericReturnType = method.getGenericReturnType();
// 判断是否进行了泛型类型参数化
if(genericReturnType instanceof ParameterizedType){
List<Object> objects = selectList(statementId,args);
return objects;
}else{
return selectOne(statementId,args);
}
}
});
return (T)proxyInstance;
}
测试流程:
@Test
public void test() throws Exception {
InputStream in = Resources.getResourcesAsStream("sqlMapConfig.xml");
SqlSessionFactory sessionFactory = new SqlSessionFactoryBuilder().build(in);
SqlSession sqlSession = sessionFactory.openSession();
// 流程测试
User user = new User();
user.setId(1);
user.setUsername("zhangsan");
User user0 = sqlSession.selectOne("com.mybatis.dao.IUserDao.findByCondition",user);
System.out.println(user0);
// 传统方式调用(需要在dao每个方法中获取sqlSession,重复代码比较多)
IUserDao iUserDao = new UserDaoImpl();
User user1 = iUserDao.findByCondition(user);
System.out.println(user1);
// 使用代理对象来调用方法(统一在外层处理一次)
IUserDao userDao = sqlSession.getMappper(IUserDao.class);
List<User> userList = userDao.findAll();
for (User user2:userList){
System.out.println(user2);
}
}
- 上一篇: MyBatis中的翻页
- 下一篇: 重学Mybatis(五)-------分页 (含面试题)
猜你喜欢
- 2024-11-21 MyBatis详解(二)
- 2024-11-21 想要开发中灵活的使用Mybatis?精通结果映射,你准了吗?
- 2024-11-21 小学妹问:Mybatis常见注解有哪些?
- 2024-11-21 重学Mybatis(二)-------主键自增 (含面试题)
- 2024-11-21 Mybatis入门
- 2024-11-21 重学Mybatis(五)-------分页 (含面试题)
- 2024-11-21 MyBatis中的翻页
- 2024-11-21 Mybatis的基础和高级查询应用实践
- 2024-11-21 看完这一篇学会MyBatis就够了
- 2024-11-21 MyBatis 总结
- 最近发表
- 标签列表
-
- cmd/c (57)
- c++中::是什么意思 (57)
- sqlset (59)
- ps可以打开pdf格式吗 (58)
- phprequire_once (61)
- localstorage.removeitem (74)
- routermode (59)
- vector线程安全吗 (70)
- & (66)
- java (73)
- org.redisson (64)
- log.warn (60)
- cannotinstantiatethetype (62)
- js数组插入 (83)
- resttemplateokhttp (59)
- gormwherein (64)
- linux删除一个文件夹 (65)
- mac安装java (72)
- reader.onload (61)
- outofmemoryerror是什么意思 (64)
- flask文件上传 (63)
- eacces (67)
- 查看mysql是否启动 (70)
- java是值传递还是引用传递 (58)
- 无效的列索引 (74)