Основная идея ORM состоит в том, чтобы упростить повторяющуюся рабочую нагрузку, установив сопоставление между MODEL и базой данных.Для простых операций добавления, удаления, модификации и запроса это может сэкономить много работы, автоматически преобразовывая MODEL в операторы SQL и Но для сложных систем требуется множество сложных операций, а SQL также должен быть сильно оптимизирован, поэтому автоматическое выполнение SQL через MODEL невозможно.
Эта проблема решается в Mybatis с помощью так называемой полуавтоматики, то есть написания SQL вручную и автоматического завершения отображения.В этой главе будет реализован простой Mybatis.
Идеи дизайна
Файл сопоставления является очень важным компонентом в MyBatis.Все операции SQL и методы сопоставления находятся в файле сопоставления.Загружайте все файлы сопоставления при запуске программы (или проекта), разрешите узел SQL и сохраните.При выполнении действия При выполнении действия соответствующий SQL найден из картографа, заполните карту параметров, выполните и вернитесь к результату выполнения.
Компоненты и интерфейсы
Поняв основные идеи дизайна, давайте приступим к разработке компонентов и интерфейсов в среде ORM.
MappedStatement
Класс информации об узле SQL отвечает за сохранение информации об узле SQL в Mapper.При загрузке файла Mapper каждый узел SQL в Mapper анализируется и сохраняется в MappedStatement.
// SQL节点信息类
public class MappedStatement {
// SQL节点ID
private String id;
// SQL语句
private String sql;
// SQL节点类型(select, insert, update, delete)
private String statementType;
// 返回值类型(类型为select时需要)
private String resultType;
// SQL中需要被映射复制的参数集合
private List<String> parameters;
// 实例化时必须传入SQL_ID
public MappedStatement(String id) {
this.id = id;
}
// Getter & Setter
// ...
}
Configuration
Глобальный класс конфигурации, сохраняет информацию об узле SQL и другую информацию о конфигурации.
// 全局配置(保存SQL节点信息及其他配置)
public class Configuration {
// 保存所有的SQL节点信息. k: SQL节点ID, v: SQL节点对象
private Map<String, MappedStatement> mappedStatements = new HashMap<String, MappedStatement>();
// 根据SQL节点ID获取SQL节点信息
public MappedStatement getMappedStatement(String id) {
return this.mappedStatements.get(id);
}
// 添加SQL节点
public MappedStatement addMappedStatement(MappedStatement s) {
return this.mappedStatements.put(s.getId(), s);
}
}
SqlSession
Компонент сеанса SQL инкапсулирует операции с базой данных и предоставляет внешний интерфейс для добавления, удаления, изменения и проверки вызовов пользователей.
// SQL会话(提供SQL基本操作)
public class SqlSession {
// 全局配置
private Configuration config;
// 实例化时必须传入全局配置
public SqlSession(Configuration config) {
this.config = config;
}
/**
* 查询单条数据(查询结果必须为一条记录)
*
* @param sqlId SQL节点ID
* @return 查询结果对应的MODEL(resultType指定的类)
*/
public <T> T selectOne(String sqlId) {
return selectOne(sqlId, null);
}
/**
* 查询单条数据(查询结果必须为一条记录)
*
* @param sqlId SQL节点ID
* @param args 执行SQL所需参数
* @return 查询结果对应的MODEL(resultType指定的类)
*/
public <T> T selectOne(String sqlId, Object args) {
List<T> list = selectList(sqlId, null);
if (list.size() > 1) {
throw new RuntimeException("count 1111");
}
if (list.size() == 1) {
return list.get(0);
}
return null;
}
/**
* 查询多条数据
*
* @param sqlId SQL节点ID
* @return 查询结果对应的MODEL(resultType指定的类)集合
*/
public <T> List<T> selectList(String sqlId) {
return selectList(sqlId, null);
}
/**
* 查询多条数据
*
* @param sqlId SQL节点ID
* @param args 执行SQL所需参数
* @return 查询结果对应的MODEL(resultType指定的类)集合
*/
public <T> List<T> selectList(String sqlId, Object args) {
// TODO
return null;
}
/**
* 插入数据
*
* @param sqlId SQL节点ID
* @return 成功被插入的数据条数
*/
public int insert(String sqlId) {
return insert(sqlId, null);
}
/**
* 插入数据
*
* @param sqlId SQL节点ID
* @param args 执行SQL所需参数
* @return 成功被插入的数据条数
*/
public int insert(String sqlId, Object args) {
return update(sqlId, args);
}
/**
* 删除数据
*
* @param sqlId SQL节点ID
* @return 成功被删除的数据条数
*/
public int delete(String sqlId) {
return delete(sqlId, null);
}
/**
* 删除数据
*
* @param sqlId SQL节点ID
* @param args 执行SQL所需参数
* @return 成功被删除的数据条数
*/
public int delete(String sqlId, Object args) {
return update(sqlId, args);
}
/**
* 更新数据
*
* @param sqlId SQL节点ID
* @return 成功被更新的数据条数
*/
public int update(String sqlId) {
return update(sqlId, null);
}
/**
* 更新数据
*
* @param sqlId SQL节点ID
* @param args 执行SQL所需参数
* @return 成功被更新的数据条数
*/
public int update(String sqlId, Object args) {
// TODO
return 0;
}
}
- вставка, удаление по сути являются обновлениями, и их не нужно реализовывать отдельно.
- В соответствии с запросом первичного ключа возвращается только одна запись, и предоставляется метод selectOne, чтобы облегчить пользователю прямую работу с МОДЕЛЬЮ (нет необходимости получать и преобразовывать из коллекции)
SqlSessionFactory Класс фабрики сеансов SQL. Отвечает за создание сеансов SQL.
// SQL会话工厂类(负责创建SQL会话)
public class SqlSessionFactory {
// 全局配置
private Configuration config;
// 实例化时必须传入全局配置
public SqlSessionFactory(Configuration config) {
this.config = config;
}
// 获取SQL会话
public SqlSession getSession() {
return new SqlSession(config);
}
}
SqlSessionFactoryBean
Запись инициализации платформы, отвечающая за синтаксический анализ файлов Mapper и создание фабрик сеансов SQL.
// 框架初始化入口(负责解析Mapper文件并创建SQL会话工厂)
public class SqlSessionFactoryBean {
private static final Pattern PARAMETER_PATTERN = Pattern.compile("#\\{(.+?)\\}");
// 全局配置
private Configuration config = new Configuration();
// Mapper文件路径
private String mapperLocation;
// 默认构造器
public SqlSessionFactoryBean() {
}
// 通过Mapper文件路径实例化
public SqlSessionFactoryBean(String mapperLocation) {
this.setMapperLocation(mapperLocation);
}
// 设置Mapper文件
public void setMapperLocation(String mapperLocation) {
this.mapperLocation = mapperLocation;
}
// 构建SQL会话工厂
public SqlSessionFactory build() {
// TODO
// 加载Mapper
return new SqlSessionFactory(config);
}
}
руководство
-
Когда программа (или проект) запускается, создайте экземпляр SqlSessionFactoryBean, задайте путь, по которому находится Mapper, и вызовите метод сборки для создания SqlSessionFactory.
-
SqlSessionFactoryBean загружает и анализирует Mapper в соответствии с путем Mapper во время сборки и инкапсулирует каждый узел SQL в Mapper в объект MappedStatement и сохраняет его в глобальной конфигурации Configuration.
-
Когда уровень доступа к данным (DAO) работает с базой данных, он получает SqlSession через SqlSessionFactory, вызывает соответствующий метод и передает идентификатор SQL.
-
SQLSESSE найден в соответствующих объектах SQL-объектам информации о распупленном запасе узла в глобальной конфигурации конфигурации в соответствии с ID SQL.
-
Входящие параметры сопоставляются в соответствии с правилами параметров SQL в MappedStatement для создания исполняемого SQL.
-
После выполнения SQL будет возвращен результат выполнения.Если это операция выбора, результат запроса будет инкапсулирован в указанный объект и возвращен
Код
SqlSessionFactoryBean используется в качестве записи инициализации платформы, загружает файл конфигурации в метод сборки, анализирует Mapper и создает фабрику сеансов SQL.
// 构建SQL会话工厂
public SqlSessionFactory build() {
if (this.mapperLocation == null) {
throw new RuntimeException("请设置Mapper文件所在路径");
}
// 获取Mapper文件
List<File> mappers = getMapperFiles();
// 加载所有Mapper并解析
for (File mapper : mappers) {
parseStatement(mapper);
}
// 返回SQL会话工厂
return new SqlSessionFactory(config);
}
Пути конфигурации файла сопоставления поддерживают подстановочные знаки (*), например:/m/*_mapper.xml
Файл Mapper находится в файле m по пути к классам и начинается с_mapper.xml
Конец. Подстановочный знак требуется для обработки при загрузке файла mappper.
// 根据Mapper所在路径获取所有Mapper文件
private List<File> getMapperFiles() {
List<File> mappers = new ArrayList<File>();
String mapperDir; // Mapper文件目录
String mapperName; // Mapper文件名称
// 根据配置的Mapper路径分别获取文件目录及文件名称
// 是否含有文件分隔符
int lastPos = this.mapperLocation.lastIndexOf("/");
// 含有文件分隔符
if (lastPos > -1) {
mapperDir = this.mapperLocation.substring(0, lastPos);
mapperName = this.mapperLocation.substring(lastPos + 1);
}
// 无文件分隔符
// 配置路径为Mapper文件名
else {
mapperDir = "";
mapperName = this.mapperLocation;
}
// 获取Mapper目录下所有文件
String classpath = ClassLoader.getSystemResource("").getPath();
File[] allMappers = new File(classpath, mapperDir).listFiles();
// *为通配符,将*转换为正则表达式通配符进行匹配
// *_mapper.xml -> .+?_mapper.xml
Pattern pattern = Pattern.compile(mapperName.replaceAll("\\*", ".+?"));
// 遍历Mapper目录下所有文件
for (File f : allMappers) {
// 文件是否和指定的Mapper名称一致
if (pattern.matcher(f.getName()).matches()) {
mappers.add(f);
}
}
return mappers;
}
Проанализируйте все узлы SQL в файле Mapper и сохраните их Формат файла Mapper следующий:
<mapper>
<select id="selectGoods" resultType="com.atd681.xc.ssm.orm.test.Goods">
select id, goods_name goodsName, category, price from t_orm_goods
</select>
<update id="updateGoods">
UPDATE t_orm_goods
SET goods_name = #{goodsName}, price = #{price}
WHERE id = #{id}
</update>
</mapper>
Инкапсулируйте каждый узел SQL в объект MappedStatement и единообразно сохраните его в глобальной конфигурации.
// 解析Mapper文件
@SuppressWarnings("unchecked")
private void parseStatement(File mapper) {
Document doc = null;
// 使用JDom解析XML
try {
doc = new SAXBuilder().build(mapper);
} catch (Exception e) {
throw new RuntimeException("加载配置文件错误", e);
}
// Mapper下所有SQL节点
List<Element> statementList = doc.getRootElement().getChildren();
// 遍历Mapper下所有SQL节点
for (Element statement : statementList) {
// SQL节点ID
String sqlId = statement.getAttributeValue("id");
// SQL节点必须设置ID属性
if (sqlId == null) {
throw new RuntimeException("SQL节点需要设置id属性");
}
// SQL节点的ID不能重复
if (config.getMappedStatement(sqlId) != null) {
throw new RuntimeException("SQL节点id已经存在");
}
// 解析SQL节点
MappedStatement ms = new MappedStatement(sqlId);
ms.setSql(statement.getTextTrim());
ms.setStatementType(statement.getName());
ms.setResultType(statement.getAttributeValue("resultType"));
// 解析SQL中的参数
parseSqlAndParameters(ms);
// 将SQL节点信息添加至全局配置中
config.addMappedStatement(ms);
}
}
Если SQL содержит параметры, которые необходимо заменить, SQL необходимо обработать Сохраните имя параметра и замените его на ?, чтобы его можно было назначить с помощью PrepareStatement при выполнении с JDBC.
// 解析SQL中的参数
private void parseSqlAndParameters(MappedStatement ms) {
List<String> parameters = new ArrayList<String>();
StringBuffer sql = new StringBuffer();
// 匹配SQL中的#{}
Matcher m = PARAMETER_PATTERN.matcher(ms.getSql());
// 将匹配到的#{}中的参数名称保存, 并替换为?
// where u_name=#{UName} and u_age=#{UAge} -> where u_name=? and u_age=?
// 执行SQL时在传入的参数中找到UName对第一个?赋值,UAge对第二个?赋值
while (m.find()) {
parameters.add(m.group(1));
m.appendReplacement(sql, "?");
}
m.appendTail(sql);
ms.setSql(sql.toString());
ms.setParameters(parameters);
}
На данный момент файл Mapper проанализирован, а также создана фабрика сеансов SQL, в DAO вы можете получить сеанс SQL и вызвать соответствующий метод операции с базой данных для выполнения.
При выполнении SQL выполняется в соответствии с назначением параметров в SQL.При разборе Mapper параметры в SQL заменены на ? и сохранено имя параметра.При назначении необходимо только получить соответствующее значение параметра через имя параметра, в свою очередь, и его можно назначить SQL через JDBC.
// 对参数进行赋值
private void setParameters(PreparedStatement ps, MappedStatement ms, Object args) throws Exception {
if (args == null) {
return;
}
List<String> parameters = ms.getParameters();
if (parameters == null) {
return;
}
// 依次根据参数名称从对应的MODEL中获取参数值替换SQL中的?
for (int i = 0; i < parameters.size(); i++) {
Object value = BeanUtil.getValue(args, parameters.get(i));
ps.setObject(i + 1, value);
}
}
Параметры, передаваемые методом DAO, вызывающим сеанс SQL, поддерживают следующие типы.
- Есть только один параметр, вы можете передать строку
- Несколько параметров могут быть переданы через Map
- Несколько параметров могут быть переданы через Javabeans
При получении соответствующего значения по имени параметра SQL необходимо отдельно разобрать три случая.
// Bean工具类
public class BeanUtil {
// 从对象中获取执行属性的值
@SuppressWarnings("rawtypes")
public static Object getValue(Object bean, String name) throws Exception {
// 字符串
if (bean instanceof String) {
return bean;
}
// Map
if (bean instanceof Map) {
return ((Map) bean).get(name);
}
// Javabean调用属性的Getter方法获取
Class<?> clazz = bean.getClass();
Method getter = clazz.getDeclaredMethod(getGetter(name), new Class<?>[] {});
return getter.invoke(bean, new Object[] {});
}
// 获取Getter方法名. userName -> getUserName
public static String getGetter(String name) {
return "get" + capitalize(name);
}
// 获取Setter方法名. userName -> setUserName
public static String getSetter(String name) {
return "set" + capitalize(name);
}
// 首字母大写. userName -> UserName
private static String capitalize(String name) {
return name.substring(0, 1).toUpperCase() + name.substring(1);
}
}
Для трех операций вставки, обновления и удаления окончательный метод выполнения в JDBC одинаков.
/**
* 更新数据
*
* @param sqlId SQL节点ID
* @param args 执行SQL所需参数
* @return 成功被更新的数据条数
*/
public int update(String sqlId, Object args) {
MappedStatement ms = config.getMappedStatement(sqlId);
Connection conn = null;
PreparedStatement ps = null;
try {
conn = getConnection();
ps = getPreparedStatement(conn, ms);
setParameters(ps, ms, args);
return ps.executeUpdate();
} catch (Exception e) {
throw new RuntimeException("", e);
} finally {
close(ps, conn);
}
}
Для операции выбора результат запроса должен быть инкапсулирован в указанный объект.
/**
* 查询多条数据
*
* @param sqlId SQL节点ID
* @param args 执行SQL所需参数
* @return 查询结果对应的MODEL(resultType指定的类)集合
*/
public <T> List<T> selectList(String sqlId, Object args) {
MappedStatement ms = config.getMappedStatement(sqlId);
Connection conn = null;
PreparedStatement ps = null;
ResultSet rs = null;
try {
conn = getConnection();
ps = getPreparedStatement(conn, ms);
setParameters(ps, ms, args);
rs = ps.executeQuery();
// 查询结果封装至指定对象中
return handleResultSet(rs, ms);
} catch (Exception e) {
throw new RuntimeException("", e);
} finally {
close(rs, ps, conn);
}
}
Класс отображения результатов запроса — это класс, определенный атрибутом resultType в узле SQL.
Имя поля результата запроса назначается автоматически, если оно согласуется с именем атрибута в классе. Если оно несовместимо, в SQL можно добавить псевдоним, чтобы сделать его согласованным с именем атрибута в классе. Например:select u_name uName from ...
// 将查询结果集封装至对象中
@SuppressWarnings("unchecked")
private <T> List<T> handleResultSet(ResultSet rs, MappedStatement ms) throws Exception {
List<T> list = new ArrayList<T>();
// ResultSetMetaData对象保存查询到的数据库相关信息.
ResultSetMetaData metaData = rs.getMetaData();
while (rs.next()) {
// 通过Java反射实例化对应的Javabean, 类型为SQL配置文件中的resultType
// 如果不设置resultType,则无法知道返回值的类型.所以resultType必须要设置.
Class<?> classObj = (Class<?>) Class.forName(ms.getResultType());
T t = (T) classObj.newInstance();
// 将每个字段的值映射到对应的对象中
int count = metaData.getColumnCount();
for (int i = 1; i <= count; i++) {
// 取得字段对象Javabean中的属性名称
String ormName = metaData.getColumnLabel(i);
// 通过属性名称,用Java反射取得Javabean中set方法.
// Javabean的定义为:所有属性为私有(private),提供共有(public)的get和set方法对其进行操作
// set方法为设置该属性的方法.set方法格式为set+属性名(首字母大写),例属性为userName,set方法为setUserName()
Class<?> filedType = classObj.getDeclaredField(ormName).getType();
Method setter = classObj.getMethod(BeanUtil.getSetter(ormName), filedType);
// 根据数据库字段的类型执行相应的set方法即可将字段值设置到属性中
setter.invoke(t, getColumnValue(rs, i));
}
list.add(t);
}
return list;
}
// 根据数据库字段类型获取其在Java中对应类型的值
private Object getColumnValue(ResultSet rs, int index) throws Exception {
int columnType = rs.getMetaData().getColumnType(index);
if (columnType == Types.BIGINT) {
return rs.getLong(index);
} else if (columnType == Types.INTEGER) {
return rs.getInt(index);
} else if (columnType == Types.VARCHAR) {
return rs.getString(index);
} else if (columnType == Types.DATE || columnType == Types.TIME || columnType == Types.TIMESTAMP) {
return rs.getDate(index);
} else if (columnType == Types.DOUBLE) {
return rs.getDouble(index);
}
return null;
}
тестовое задание
UserMODEL
// 用户MODEL
public class User {
// id
private Long id;
// 用户名
private String uname;
// 用户年龄
private Integer uage;
// 用户地址
private String uaddr;
// 备注
private String remark;
public User() {
}
// 通过用户信息构造用户
public User(Long id, String uname, Integer uage, String uaddr, String remark) {
this.id = id;
this.uname = uname;
this.uage = uage;
this.uaddr = uaddr;
this.remark = remark;
}
// Getter & Setter
// ...
}
Пользовательский интерфейс DAO
// 用户DAO接口
public interface UserDAO {
// 创建用户
int insertUser(User user);
// 更新用户
int updateUser(User user);
// 查询用户
List<User> selectUser();
}
Пользовательский класс реализации DAO
// 用户DAO
public class UserDAOImpl extends BaseDAO implements UserDAO {
// SQL会话工厂在父类中
public UserDAOImpl(SqlSessionFactory sf) {
super(sf);
}
// 添加用户
public int insertUser(User user) {
return getSqlSession().update("insertUser", user);
}
// 更新用户
public int updateUser(User user) {
return getSqlSession().update("updateUser", user);
}
// 查询用户
public List<User> selectUser() {
return getSqlSession().selectList("selectUser");
}
}
Чтобы упростить операции DAO, все DAO наследуют BaseDAO, где BaseDAO отвечает за сохранение фабрики сеансов SQL и предоставление методов для получения сеансов SQL.
// DAO基类
public class BaseDAO {
// SQL会话工厂
private SqlSessionFactory sf;
// 通过SQL会话工厂实例化
public BaseDAO(SqlSessionFactory sf) {
this.sf = sf;
}
// 获取SQL会话
protected SqlSession getSqlSession() {
return sf.getSession();
}
}
Создайте новый тестовый класс, задайте местоположение Mapper и инициализируйте структуру ORM, а затем выполните операции DAO для вывода результатов.
public class TestImpl {
public static void main(String[] args) {
// 创建SQL会话工厂
SqlSessionFactory sf = new SqlSessionFactoryBean("*_mapper.xml").build();
// 实例化DAO
UserDAO userDAO = new UserDAOImpl(sf);
// 调用DAO中方法
int count = userDAO.insertUser(new User(1L, "zhangsan", 20, "sssss", "ok"));
List<User> userList = userDAO.selectUser();
// 输出查询结果
System.out.println(count);
for (User u : userList) {
System.out.println("| " + u.getId() + " | " + u.getUname() + " | ");
}
}
}