mybatis如何工作

如果自己能夠寫一個模仿mybatis工作的程序,那麼看mybatis的源碼就會很容易。

pom

  <dependencies>
	  <dependency>
		  <groupId>dom4j</groupId>
		  <artifactId>dom4j</artifactId>
		  <version>1.6.1</version>
	  </dependency>
	  <dependency>
		  <groupId>mysql</groupId>
		  <artifactId>mysql-connector-java</artifactId>
		  <version>5.1.47</version>
	  </dependency>
	  <dependency>
		  <groupId>junit</groupId>
		  <artifactId>junit</artifactId>
		  <version>4.12</version>
		  <scope>test</scope>
	  </dependency>
  </dependencies>

我們沒有依賴mybatis,但是我們要實現和mybatis類似的功能,就是直接調用mapper接口進行查詢。

我們的包結構:

配置文件與java類的映射

java一切皆對象。

我們首先要把db.propertiesuserMapper.xml給讀進來存儲到對象中。

db.properties的對象叫做Configuration

public class Configuration {
	private String driver;
	private String url;
	private String username;
	private String password;

	private Map<String,MapperStatement> map = new HashMap<String, MapperStatement>();
	
	public String getDriver() {
		return driver;
	}

	public void setDriver(String driver) {
		this.driver = driver;
	}

	public String getUrl() {
		return url;
	}

	public void setUrl(String url) {
		this.url = url;
	}

	public String getUsername() {
		return username;
	}

	public void setUsername(String username) {
		this.username = username;
	}

	public String getPassword() {
		return password;
	}

	public void setPassword(String password) {
		this.password = password;
	}

	public Map<String, MapperStatement> getMap() {
		return map;
	}

	@Override
	public String toString() {
		return "Configuration{" +
				"driver='" + driver + '\'' +
				", url='" + url + '\'' +
				", username='" + username + '\'' +
				", password='" + password + '\'' +
				", map=" + map +
				'}';
	}
}

userMapper.xml的對象叫做MapperStatement

public class MapperStatement {
	private String namespace;
	private String id;
	private String resultType;
	private String sql;

	public String getNamespace() {
		return namespace;
	}

	public void setNamespace(String namespace) {
		this.namespace = namespace;
	}

	public String getId() {
		return id;
	}

	public void setId(String id) {
		this.id = id;
	}

	public String getResultType() {
		return resultType;
	}

	public void setResultType(String resultType) {
		this.resultType = resultType;
	}

	public String getSql() {
		return sql;
	}

	public void setSql(String sql) {
		this.sql = sql;
	}

	@Override
	public String toString() {
		return "MapperStatement{" +
				"namespace='" + namespace + '\'' +
				", id='" + id + '\'' +
				", resultType='" + resultType + '\'' +
				", sql='" + sql + '\'' +
				'}';
	}
}

注意在Configuration中,有一個屬性new HashMap<String, MapperStatement>(),它的作用是通過key找到唯一的MapperStatement

那麼需要哪些條件才能做到呢?

只需要namespaceid就行。知道這兩個值,就能找到唯一確定的sql(找到MapperStatement也是爲了找到sql)。

我們的Configuration最終將長這個樣子:

Configuration
{driver='com.mysql.jdbc.Driver', 

url='jdbc:mysql://localhost:3306/mybatis?userUnicode=true&characterEncoding=utf-8', 

username='root', 

password='123456', 

map={
com.ocean.mapper.UserMapper.findAll=MapperStatement{
namespace='com.ocean.mapper.UserMapper', id='findAll', resultType='com.ocean.entity.User', 
sql='
		select * from t_user;
	'}, 

com.ocean.mapper.UserMapper.selectByPrimaryKey=MapperStatement{
namespace='com.ocean.mapper.UserMapper', id='selectByPrimaryKey', resultType='com.ocean.entity.User', 
sql='
	select id, username from t_user where id = ?
'}
	}
		}

加載配置文件

加載配置文件要在最開始做,也就是初始SqlSessionFactory的時候做:

/**
 * load db.properties and xxxmapper.xml
 */
public class SqlSessionFactory {
	private Configuration configuration;
	private static final String MAPPER_FOLDER = "./mapper";
	private static final String DB_LOCATION = "./db.properties";

	public SqlSessionFactory() {
		configuration = new Configuration();
		loadDBInfo();
		loadMappers();
		System.out.println("init sql session factory, the configuration is : " + configuration);
	}

	public SqlSession openSession(){
		return new DefaultSession(configuration);
	}

	private void loadMappers() {
		URL resource = SqlSessionFactory.class.getClassLoader().getResource(MAPPER_FOLDER);
		File mapper = new File(resource.getFile());
		if (mapper.isDirectory()) {
			File[] files = mapper.listFiles();
			for (File file : files) {
				loadMapper(file);
			}
		}

	}

	private void loadMapper(File file) {
		SAXReader reader = new SAXReader();
		try {
			Document document = reader.read(file);
			Element rootElement = document.getRootElement();
			String namespace = rootElement.attributeValue("namespace");
			List<Element> elements = rootElement.elements();
			for (Element element : elements) {
				MapperStatement mapperStatement = new MapperStatement();
				String id = element.attributeValue("id");
				String resultType = element.attributeValue("resultType");
				String sql = element.getData().toString();
				mapperStatement.setId(id);
				mapperStatement.setNamespace(namespace);
				mapperStatement.setResultType(resultType);
				mapperStatement.setSql(sql);

				configuration.getMap().put(namespace + "." + id, mapperStatement);
			}
		}
		catch (Exception e) {
			e.printStackTrace();
		}
	}

	private void loadDBInfo() {
		InputStream inputStream = SqlSessionFactory.class.getClassLoader().getResourceAsStream(DB_LOCATION);
		Properties properties = new Properties();

		try {
			properties.load(inputStream);
			configuration.setDriver(properties.getProperty("driver"));
			configuration.setUrl(properties.getProperty("url"));
			configuration.setUsername(properties.getProperty("username"));
			configuration.setPassword(properties.getProperty("password"));
		}
		catch (Exception e) {
			e.printStackTrace();
		}
	}


}

這裏就是用Properties讀取數據庫連接數據:

driver=com.mysql.jdbc.Driver
url=jdbc:mysql://localhost:3306/mybatis?userUnicode=true&characterEncoding=utf-8
username=root
password=123456

SAXReader讀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.ocean.mapper.UserMapper">
	<!--User selectByPrimaryKey(Integer id);-->
<select id="selectByPrimaryKey" resultType="com.ocean.entity.User">
	select id, username from t_user where id = ?
</select>

<!--List<User> findAll();-->
	<select id="findAll" resultType="com.ocean.entity.User">
		select * from t_user;
	</select>

</mapper>

爲了測試,我們只准備了兩個查詢。

這時候我正好可以介紹實體類和mapper接口:

User

public class User implements Serializable {
	private Integer id;
	private String username;

	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;
	}

	@Override
	public String toString() {
		return "User{" +
				"id=" + id +
				", username='" + username + '\'' +
				'}';
	}
}

mapper

public interface UserMapper {

	User selectByPrimaryKey(Integer id);

	List<User> findAll();
}

nothing special。

執行sql併爲實體類填充值

在不知道下一步怎麼辦的情況下,我們知道,用jdbc查sql這一步是逃不了的。

所以,先寫jdbc。

public interface Executor {
	<T>List<T> query(MapperStatement mapperStatement, Object parameter);
}

我們傳進MapperStatement和參數。並統一返回list。

/**
 * given the configuration loaded by SqlSessionFactory, execute the standard query using jdbc
 */
public class DefaultExecutor implements Executor {

	private Configuration configuration;

	public DefaultExecutor(Configuration configuration) {
		this.configuration = configuration;
	}

	/**
	 * a standard query using jdbc
	 * @param mapperStatement
	 * @param parameter
	 * @param <T>
	 * @return
	 */
	@Override
	public <T> List<T> query(MapperStatement mapperStatement, Object parameter) {
		List<T> list = new ArrayList<T>();
		Connection connection = null;
		PreparedStatement preparedStatement = null;
		ResultSet resultSet = null;

		try {
			Class.forName(configuration.getDriver());

			connection = DriverManager
					.getConnection(configuration.getUrl(), configuration.getUsername(), configuration.getPassword());
			preparedStatement = connection.prepareStatement(mapperStatement.getSql());
			System.out.println("prepared statement is : " + preparedStatement);
			parameterize(preparedStatement, parameter);
			resultSet = preparedStatement.executeQuery();
			handleResultset(list, resultSet, mapperStatement);

			System.out.println("list containing entity is : " + list);

		}
		catch (Exception e) {
			e.printStackTrace();
		}
		finally {
			if (null != connection) {
				try {
					connection.close();
				}
				catch (SQLException e) {
					e.printStackTrace();
				}
			}
			if (null != preparedStatement) {
				try {
					preparedStatement.close();
				}
				catch (SQLException e) {
					e.printStackTrace();
				}
			}
			if (null != resultSet) {
				try {
					resultSet.close();
				}
				catch (SQLException e) {
					e.printStackTrace();
				}
			}
		}
		return list;
	}

	/**
	 * set the result set value to the entity field
	 * @param list
	 * @param resultSet
	 * @param mapperStatement
	 * @param <T>
	 * @throws Exception
	 */
	private <T> void handleResultset(List<T> list, ResultSet resultSet, MapperStatement mapperStatement) throws Exception {
		Class<?> clazz = Class.forName(mapperStatement.getResultType());
		while (resultSet.next()) {
			Object entity = clazz.newInstance();
			ReflectionUtil.setPropertyFromResultSet(entity, resultSet);
			list.add((T) entity);
		}
	}

	/**
	 * set value to prepared statement with parameter
	 * @param preparedStatement
	 * @param parameter
	 * @throws Exception
	 */
	private void parameterize(PreparedStatement preparedStatement, Object parameter) throws Exception {
		if (parameter instanceof String) {
			preparedStatement.setString(1, (String) parameter);
		}
		else if (parameter instanceof Integer) {
			preparedStatement.setInt(1, (Integer) parameter);
		}
	}
}

由於我們不知道傳進來的sql是怎麼樣的,所以我們要用parameterize方法動態地處理參數。

查出來的結果在ResultSet中,我們要把它的值賦給實體類。

我們使用了一個工具類:ReflectionUtil.setPropertyFromResultSet(entity, resultSet);

public class ReflectionUtil {

	/**
	 * take the value of result set out, and then give it to the entity field
	 * @param entity
	 * @param resultSet
	 * @throws Exception
	 */
	public static void setPropertyFromResultSet(Object entity, ResultSet resultSet)throws Exception{
		Field[] declaredFields = entity.getClass().getDeclaredFields();

		for (int i = 0; i < declaredFields.length; i++) {
			if (declaredFields[i].getType().getSimpleName().equals("String")) {
				setPropertyToEntity(entity,declaredFields[i].getName(),resultSet.getString(declaredFields[i].getName()));
			}else if(declaredFields[i].getType().getSimpleName().equals("Integer")){
				setPropertyToEntity(entity,declaredFields[i].getName(),resultSet.getInt(declaredFields[i].getName()));
			}
		}
	}
	
	/**
	 * set value of field for entity
	 * @param entity
	 * @param name
	 * @param value
	 * @throws Exception
	 */
	private static void setPropertyToEntity(Object entity, String name, Object value) throws Exception{
		Field field;
		field = entity.getClass().getDeclaredField(name);
		field.setAccessible(true);
		field.set(entity,value);
	}
}

它的目的就是把resultset中的值一一取出來,給User賦上。

sqlsession

該做的工作都做好了。

暴露給程序員的就是一個session,它代表着與數據庫的一次連接。

我們看一下之前mybatis是怎麼做的:

  public static SqlSessionFactory getSqlSessionFactory() throws Exception {
        String resources = "mybatis-config.xml";
        InputStream resourceAsStream = Resources.getResourceAsStream(resources);
        return new SqlSessionFactoryBuilder().build(resourceAsStream);
    }

    @Test
    public void testCreateEmp() throws Exception {
        SqlSessionFactory sqlSessionFactory = getSqlSessionFactory();
        SqlSession sqlSession = sqlSessionFactory.openSession();
        try {
            EmployeeMapper employeeMapper = sqlSession.getMapper(EmployeeMapper.class);
            Employee employee = new Employee();
            employee.setLastName("Yeats");
            employee.setEmail("[email protected]");
            Integer result = employeeMapper.createEmployee(employee);
            System.out.println("if created successfully? " + result);

            sqlSession.commit();
        } finally {
            sqlSession.close();
        }

    }

sqlSession有一個getMapper的方法,並且返回一個代理對象。

並且帶代理對象去執行具體的增刪改查方法。

如果是用jdk動態代理來做的,當上述代碼執行createEmployee方法時,一定會被一個InvocationHandler攔截到,並且執行invoke方法。

我的思路是把真正要執行的查詢放在invoke方法中,這個增刪改查工作是由SqlSession完成的,當然,SqlSession肯定封裝了底層幹活的Executor

上面是大體的思路。

首先我們需要一個SqlSession

public interface SqlSession {
	<T> T selectById(String sourceId, Object parameter);
	<T>List<T> selectAll(String sourceId, Object parameter);
	<T> T getMapper(Class<T> type);
}

它一定會有getMapper方法,一定會有session層面的增刪改查方法。

然後是實現類:

/**
 * default session implementation
 *
 * @author
 */
public class DefaultSession implements SqlSession {

	private Configuration configuration;
	private Executor executor;

	public DefaultSession(Configuration configuration){
		this.configuration=configuration;
		executor=new DefaultExecutor(configuration);
	}

	@Override
	public <T> T selectById(String sourceId, Object parameter) {
		List<Object> objects = selectAll(sourceId, parameter);
		if(objects==null||objects.size()<0){
			return null;
		}else if(objects.size()>1){
			throw new RuntimeException("too many results");
		}else {
			Object ret = objects.get(0);
			return (T) ret;
		}
	}

	@Override
	public <T> List<T> selectAll(String sourceId, Object parameter) {
		MapperStatement mapperStatement = configuration.getMap().get(sourceId);
		return executor.query(mapperStatement,parameter);
	}

	@Override
	public <T> T getMapper(Class<T> type) {
		MapperHandler mapperHandler = new MapperHandler(this);
		Object proxyInstance = Proxy.newProxyInstance(type.getClassLoader(), new Class[] {type}, mapperHandler);
		return (T)proxyInstance;
	}
}

先看selectByIdselectAll這兩個方法,它們最終是讓Executor去幹活的,這和我們的設計思路是一致的。

然後是getMapper。我們用Proxy.newProxyInstance的方式返回一個代理。

所需要的參數是一個系統類加載器,一個接口對象,一個InvocationHandler

我們把InvocationHandler寫出來:

/**
 * the handler of a proxy instance of a mapper
 */
public class MapperHandler implements InvocationHandler {
	private SqlSession sqlSession;

	public MapperHandler(SqlSession sqlSession) {
		this.sqlSession = sqlSession;
	}

	@Override
	public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
		if ("java.lang.Object".equals(method.getDeclaringClass().getName())) {
			return null;
		}

		System.out.println("sourceId : " + method.getDeclaringClass().getName() + "." + method.getName());

		//if return type is instance of Collection(in our case , it's a list), execute selectAll
		if (Collection.class.isAssignableFrom(method.getReturnType())) {
			return sqlSession.selectAll(method.getDeclaringClass().getName() + "." + method.getName(), args==null?null:args[0]);
		}
		else {
			return sqlSession.selectById(method.getDeclaringClass().getName() + "." + method.getName(), args==null?null:args[0]);
		}
	}
}

invoke方法裏,我們委託sqlSession幹活。

if ("java.lang.Object".equals(method.getDeclaringClass().getName())) 
{
	return null;
}

這句很奇怪,因爲程序有bug,我只有這麼加纔能有結果,知道bug原因的朋友可以告訴我。

Test

public class MyBatisTest {

	public static void main(String[] args) throws Exception {
		SqlSessionFactory sqlSessionFactory = new SqlSessionFactory();
		SqlSession session = sqlSessionFactory.openSession();

		UserMapper userMapper = session.getMapper(UserMapper.class);
		List<User> all = userMapper.findAll();
		all.forEach(System.out::println);

		/*User userOne = userMapper.selectByPrimaryKey(1);
		System.out.println("userOne : "+userOne);*/
	}
}

得到正確結果。完事。

但是Proxy.newProxyInstance(type.getClassLoader(), new Class[] {type}, mapperHandler);這段是有問題的。

我debug了一下,proxy0生成了,構造器也順利拿到了,就是

 return cons.newInstance(new Object[]{h});

返回了一個null。

知道爲什麼的朋友可以告訴我。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章