重學 Java 設計模式:實戰中介者模式「按照Mybaits原理手寫ORM框架,給JDBC方式操做數據庫增長中介者場景」

image

做者:小傅哥
博客:https://bugstack.cn - 原創系列專題文章html

沉澱、分享、成長,讓本身和他人都能有所收穫!😄

1、前言

同齡人的差距是從何時拉開的java

一樣的幼兒園、一樣的小學、同樣的書本、同樣的課堂,有人學習好、有人學習差。不僅是上學,幾乎人生到處都是賽道,發令槍響起的時刻,也就把人生的差距拉開。編程開發這條路也是很長很寬,有人跑得快有人跑得慢。那麼你是否想起過,這一點點的差距到高不可攀的距離,是從哪一天開始的。摸摸肚子的肉,看看遠處的路,別人講的是故事,你想起的都是事故node

思想沒有產品高才寫出一片的ifelsemysql

當你承接一個需求的時候,好比;交易、訂單、營銷、保險等各種場景。若是你不熟悉這個場景下的業務模式,以及未來的拓展方向,那麼很難設計出良好可擴展的系統。再加上產品功能初建,說老闆要的急,儘快上線。做爲程序員的你更沒有時間思考,總體一看如今的需求也不難,直接上手開幹(一個方法兩個if語句),這樣確實知足了當前需求。但老闆的想法多呀,產品也跟着變化快,到你這就是改改改,加加加。固然你也不客氣,回首掏就是1024個if語句!程序員

日積月累的技術沉澱是爲了厚積薄發正則表達式

粗略的估算過,若是從上大學開始天天寫200行,一個月是6000行,一年算10個月話,就是6萬行,第三年出去實習的是時候就有20萬行的代碼量。若是你能作到這一點,找工做難?有時候不少事情就是靠時間積累出來的,想走捷徑有時候真的沒有。你的技術水平、你的業務能力、你身上的肉,都是一點點積累下來的,不要浪費看似很短的時間,一年年堅持下來,留下印刻青春的痕跡,多給本身武裝上一些能力。sql

2、開發環境

  1. JDK 1.8
  2. Idea + Maven
  3. mysql 5.1.20
  4. 涉及工程一個,能夠經過關注公衆號bugstack蟲洞棧,回覆源碼下載獲取(打開獲取的連接,找到序號18)
工程 描述
itstack-demo-design-16-01 使用JDBC方式鏈接數據庫
itstack-demo-design-16-02 手寫ORM框架操做數據庫

3、中介者模式介紹

中介者模式,圖片來自 refactoringguru.cn

中介者模式要解決的就是複雜功能應用之間的重複調用,在這中間添加一層中介者包裝服務,對外提供簡單、通用、易擴展的服務能力。數據庫

這樣的設計模式幾乎在咱們平常生活和實際業務開發中都會見到,例如;飛機🛬降落有小姐姐在塔臺喊話、不管哪一個方向來的候車都從站臺上下、公司的系統中有一箇中臺專門爲你包裝全部接口和提供統一的服務等等,這些都運用了中介者模式。除此以外,你用到的一些中間件,他們包裝了底層多種數據庫的差別化,提供很是簡單的方式進行使用。編程

4、案例場景模擬

場景模擬;模仿Mybatis手寫ORM框架

在本案例中咱們經過模仿Mybatis手寫ORM框架,經過這樣操做數據庫學習中介者運用場景設計模式

除了這樣的中間件層使用場景外,對於一些外部接口,例如N種獎品服務,能夠由中臺系統進行統一包裝對外提供服務能力。也是中介者模式的一種思想體現。

在本案例中咱們會把jdbc層進行包裝,讓用戶在使用數據庫服務的時候,能夠和使用mybatis同樣簡單方便,經過這樣的源碼方式學習中介者模式,也方便對源碼知識的拓展學習,加強知識棧。

5、用一坨坨代碼實現

這是一種關於數據庫操做最初的方式

基本上每個學習開發的人都學習過直接使用jdbc方式鏈接數據庫,進行CRUD操做。如下的例子能夠當作回憶。

1. 工程結構

itstack-demo-design-16-01
└── src
    └── main
        └── java
            └── org.itstack.demo.design
                └── JDBCUtil.java
  • 這裏的類比較簡單隻包括了一個數據庫操做類。

2. 代碼實現

public class JDBCUtil {

    private static Logger logger = LoggerFactory.getLogger(JDBCUtil.class);

    public static final String URL = "jdbc:mysql://127.0.0.1:3306/itstack-demo-design";
    public static final String USER = "root";
    public static final String PASSWORD = "123456";

    public static void main(String[] args) throws Exception {
        //1. 加載驅動程序
        Class.forName("com.mysql.jdbc.Driver");
        //2. 得到數據庫鏈接
        Connection conn = DriverManager.getConnection(URL, USER, PASSWORD);
        //3. 操做數據庫
        Statement stmt = conn.createStatement();
        ResultSet resultSet = stmt.executeQuery("SELECT id, name, age, createTime, updateTime FROM user");
        //4. 若是有數據 resultSet.next() 返回true
        while (resultSet.next()) {
            logger.info("測試結果 姓名:{} 年齡:{}", resultSet.getString("name"),resultSet.getInt("age"));
        }
    }

}
  • 以上是使用JDBC的方式進行直接操做數據庫,幾乎你們都使用過這樣的方式。

3. 測試結果

15:38:10.919 [main] INFO  org.itstack.demo.design.JDBCUtil - 測試結果 姓名:水水 年齡:18
15:38:10.922 [main] INFO  org.itstack.demo.design.JDBCUtil - 測試結果 姓名:豆豆 年齡:18
15:38:10.922 [main] INFO  org.itstack.demo.design.JDBCUtil - 測試結果 姓名:花花 年齡:19

Process finished with exit code 0
  • 從測試結果能夠看到這裏已經查詢到了數據庫中的數據。只不過若是在所有的業務開發中都這樣實現,會很是的麻煩。

6、中介模式開發ORM框架

`接下來就使用中介模式的思想完成模仿Mybatis的ORM框架開發~

1. 工程結構

itstack-demo-design-16-02
└── src
    ├── main
    │   ├── java
    │   │   └── org.itstack.demo.design
    │   │       ├── dao
    │   │       │    ├── ISchool.java
    │   │       │    └── IUserDao.java
    │   │       ├── mediator
    │   │       │    ├── Configuration.java
    │   │       │    ├── DefaultSqlSession.java
    │   │       │    ├── DefaultSqlSessionFactory.java
    │   │       │    ├── Resources.java
    │   │       │    ├── SqlSession.java
    │   │       │    ├── SqlSessionFactory.java
    │   │       │    ├── SqlSessionFactoryBuilder.java
    │   │       │    └── SqlSessionFactoryBuilder.java
    │   │       └── po
    │   │             ├── School.java
    │   │             └── User.java
    │   └── resources
    │       ├── mapper
    │       │   ├── School_Mapper.xml
    │       │   └── User_Mapper.xml
    │       └── mybatis-config-datasource.xml
    └── test
         └── java
             └── org.itstack.demo.design.test
                 └── ApiTest.java

中介者模式模型結構

中介者模式模型結構

  • 以上是對ORM框架實現的核心類,包括了;加載配置文件、對xml解析、獲取數據庫session、操做數據庫以及結果返回。
  • 左上是對數據庫的定義和處理,基本包括咱們經常使用的方法;<T> T selectOne<T> List<T> selectList等。
  • 右側藍色部分是對數據庫配置的開啓session的工廠處理類,這裏的工廠會操做DefaultSqlSession
  • 以後是紅色地方的SqlSessionFactoryBuilder,這個類是對數據庫操做的核心類;處理工廠、解析文件、拿到session等。

接下來咱們就分別介紹各個類的功能實現過程。

2. 代碼實現

2.1 定義SqlSession接口

public interface SqlSession {

    <T> T selectOne(String statement);

    <T> T selectOne(String statement, Object parameter);

    <T> List<T> selectList(String statement);

    <T> List<T> selectList(String statement, Object parameter);

    void close();
}
  • 這裏定義了對數據庫操做的查詢接口,分爲查詢一個結果和查詢多個結果,同時包括有參數和沒有參數的方法。

2.2 SqlSession具體實現類

public class DefaultSqlSession implements SqlSession {

    private Connection connection;
    private Map<String, XNode> mapperElement;

    public DefaultSqlSession(Connection connection, Map<String, XNode> mapperElement) {
        this.connection = connection;
        this.mapperElement = mapperElement;
    }

    @Override
    public <T> T selectOne(String statement) {
        try {
            XNode xNode = mapperElement.get(statement);
            PreparedStatement preparedStatement = connection.prepareStatement(xNode.getSql());
            ResultSet resultSet = preparedStatement.executeQuery();
            List<T> objects = resultSet2Obj(resultSet, Class.forName(xNode.getResultType()));
            return objects.get(0);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    @Override
    public <T> List<T> selectList(String statement) {
        XNode xNode = mapperElement.get(statement);
        try {
            PreparedStatement preparedStatement = connection.prepareStatement(xNode.getSql());
            ResultSet resultSet = preparedStatement.executeQuery();
            return resultSet2Obj(resultSet, Class.forName(xNode.getResultType()));
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    // ...

    private <T> List<T> resultSet2Obj(ResultSet resultSet, Class<?> clazz) {
        List<T> list = new ArrayList<>();
        try {
            ResultSetMetaData metaData = resultSet.getMetaData();
            int columnCount = metaData.getColumnCount();
            // 每次遍歷行值
            while (resultSet.next()) {
                T obj = (T) clazz.newInstance();
                for (int i = 1; i <= columnCount; i++) {
                    Object value = resultSet.getObject(i);
                    String columnName = metaData.getColumnName(i);
                    String setMethod = "set" + columnName.substring(0, 1).toUpperCase() + columnName.substring(1);
                    Method method;
                    if (value instanceof Timestamp) {
                        method = clazz.getMethod(setMethod, Date.class);
                    } else {
                        method = clazz.getMethod(setMethod, value.getClass());
                    }
                    method.invoke(obj, value);
                }
                list.add(obj);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return list;
    }

    @Override
    public void close() {
        if (null == connection) return;
        try {
            connection.close();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}
  • 這裏包括了接口定義的方法實現,也就是包裝了jdbc層。
  • 經過這樣的包裝可讓對數據庫的jdbc操做隱藏起來,外部調用的時候對入參、出參都有內部進行處理。

2.3 定義SqlSessionFactory接口

public interface SqlSessionFactory {

    SqlSession openSession();

}
  • 開啓一個SqlSession, 這幾乎是你們在平時的使用中都須要進行操做的內容。雖然你看不見,可是當你有數據庫操做的時候都會獲取每一次執行的SqlSession

2.4 SqlSessionFactory具體實現類

public class DefaultSqlSessionFactory implements SqlSessionFactory {

    private final Configuration configuration;

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

    @Override
    public SqlSession openSession() {
        return new DefaultSqlSession(configuration.connection, configuration.mapperElement);
    }

}
  • DefaultSqlSessionFactory,是使用mybatis最經常使用的類,這裏咱們簡單的實現了一個版本。
  • 雖然是簡單的版本,可是包括了最基本的核心思路。當開啓SqlSession時會進行返回一個DefaultSqlSession
  • 這個構造函數中向下傳遞了Configuration配置文件,在這個配置文件中包括;Connection connectionMap<String, String> dataSourceMap<String, XNode> mapperElement。若是有你閱讀過Mybatis源碼,對這個就不會陌生。

2.5 SqlSessionFactoryBuilder實現

public class SqlSessionFactoryBuilder {

    public DefaultSqlSessionFactory build(Reader reader) {
        SAXReader saxReader = new SAXReader();
        try {
            saxReader.setEntityResolver(new XMLMapperEntityResolver());
            Document document = saxReader.read(new InputSource(reader));
            Configuration configuration = parseConfiguration(document.getRootElement());
            return new DefaultSqlSessionFactory(configuration);
        } catch (DocumentException e) {
            e.printStackTrace();
        }
        return null;
    }

    private Configuration parseConfiguration(Element root) {
        Configuration configuration = new Configuration();
        configuration.setDataSource(dataSource(root.selectNodes("//dataSource")));
        configuration.setConnection(connection(configuration.dataSource));
        configuration.setMapperElement(mapperElement(root.selectNodes("mappers")));
        return configuration;
    }

    // 獲取數據源配置信息
    private Map<String, String> dataSource(List<Element> list) {
        Map<String, String> dataSource = new HashMap<>(4);
        Element element = list.get(0);
        List content = element.content();
        for (Object o : content) {
            Element e = (Element) o;
            String name = e.attributeValue("name");
            String value = e.attributeValue("value");
            dataSource.put(name, value);
        }
        return dataSource;
    }

    private Connection connection(Map<String, String> dataSource) {
        try {
            Class.forName(dataSource.get("driver"));
            return DriverManager.getConnection(dataSource.get("url"), dataSource.get("username"), dataSource.get("password"));
        } catch (ClassNotFoundException | SQLException e) {
            e.printStackTrace();
        }
        return null;
    }

    // 獲取SQL語句信息
    private Map<String, XNode> mapperElement(List<Element> list) {
        Map<String, XNode> map = new HashMap<>();

        Element element = list.get(0);
        List content = element.content();
        for (Object o : content) {
            Element e = (Element) o;
            String resource = e.attributeValue("resource");

            try {
                Reader reader = Resources.getResourceAsReader(resource);
                SAXReader saxReader = new SAXReader();
                Document document = saxReader.read(new InputSource(reader));
                Element root = document.getRootElement();
                //命名空間
                String namespace = root.attributeValue("namespace");

                // SELECT
                List<Element> selectNodes = root.selectNodes("select");
                for (Element node : selectNodes) {
                    String id = node.attributeValue("id");
                    String parameterType = node.attributeValue("parameterType");
                    String resultType = node.attributeValue("resultType");
                    String sql = node.getText();

                    // ? 匹配
                    Map<Integer, String> parameter = new HashMap<>();
                    Pattern pattern = Pattern.compile("(#\\{(.*?)})");
                    Matcher matcher = pattern.matcher(sql);
                    for (int i = 1; matcher.find(); i++) {
                        String g1 = matcher.group(1);
                        String g2 = matcher.group(2);
                        parameter.put(i, g2);
                        sql = sql.replace(g1, "?");
                    }

                    XNode xNode = new XNode();
                    xNode.setNamespace(namespace);
                    xNode.setId(id);
                    xNode.setParameterType(parameterType);
                    xNode.setResultType(resultType);
                    xNode.setSql(sql);
                    xNode.setParameter(parameter);

                    map.put(namespace + "." + id, xNode);
                }
            } catch (Exception ex) {
                ex.printStackTrace();
            }

        }
        return map;
    }

}
  • 在這個類中包括的核心方法有;build(構建實例化元素)parseConfiguration(解析配置)dataSource(獲取數據庫配置)connection(Map<String, String> dataSource) (連接數據庫)mapperElement (解析sql語句)
  • 接下來咱們分別介紹這樣的幾個核心方法。

build(構建實例化元素)

這個類主要用於建立解析xml文件的類,以及初始化SqlSession工廠類DefaultSqlSessionFactory。另外須要注意這段代碼saxReader.setEntityResolver(new XMLMapperEntityResolver());,是爲了保證在不聯網的時候同樣能夠解析xml,不然會須要從互聯網獲取dtd文件。

parseConfiguration(解析配置)

是對xml中的元素進行獲取,這裏主要獲取了;dataSourcemappers,而這兩個配置一個是咱們數據庫的連接信息,另一個是對數據庫操做語句的解析。

connection(Map<String, String> dataSource) (連接數據庫)

連接數據庫的地方和咱們常見的方式是同樣的;Class.forName(dataSource.get("driver"));,可是這樣包裝之後外部是不須要知道具體的操做。同時當咱們須要連接多套數據庫的時候,也是能夠在這裏擴展。

mapperElement (解析sql語句)

這部分代碼塊內容相對來講比較長,可是核心的點就是爲了解析xml中的sql語句配置。在咱們日常的使用中基本都會配置一些sql語句,也有一些入參的佔位符。在這裏咱們使用正則表達式的方式進行解析操做。

解析完成的sql語句就有了一個名稱和sql的映射關係,當咱們進行數據庫操做的時候,這個組件就能夠經過映射關係獲取到對應sql語句進行操做。

3. 測試驗證

在測試以前須要導入sql語句到數據庫中;

  • 庫名:itstack-demo-design
  • 表名:userschool
CREATE TABLE school ( id bigint NOT NULL AUTO_INCREMENT, name varchar(64), address varchar(256), createTime datetime, updateTime datetime, PRIMARY KEY (id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
insert into school (id, name, address, createTime, updateTime) values (1, '北京大學', '北京市海淀區頤和園路5號', '2019-10-18 13:35:57', '2019-10-18 13:35:57');
insert into school (id, name, address, createTime, updateTime) values (2, '南開大學', '中國天津市南開區衛津路94號', '2019-10-18 13:35:57', '2019-10-18 13:35:57');
insert into school (id, name, address, createTime, updateTime) values (3, '同濟大學', '上海市彰武路1號同濟大廈A樓7樓7區', '2019-10-18 13:35:57', '2019-10-18 13:35:57');
CREATE TABLE user ( id bigint(11) NOT NULL AUTO_INCREMENT, name varchar(32), age int(4), address varchar(128), entryTime datetime, remark varchar(64), createTime datetime, updateTime datetime, status int(4) DEFAULT '0', dateTime varchar(64), PRIMARY KEY (id), INDEX idx_name (name) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
insert into user (id, name, age, address, entryTime, remark, createTime, updateTime, status, dateTime) values (1, '水水', 18, '吉林省榆樹市黑林鎮尹家村5組', '2019-12-22 00:00:00', '無', '2019-12-22 00:00:00', '2019-12-22 00:00:00', 0, '20200309');
insert into user (id, name, age, address, entryTime, remark, createTime, updateTime, status, dateTime) values (2, '豆豆', 18, '遼寧省大連市清河灣司馬道407路', '2019-12-22 00:00:00', '無', '2019-12-22 00:00:00', '2019-12-22 00:00:00', 1, null);
insert into user (id, name, age, address, entryTime, remark, createTime, updateTime, status, dateTime) values (3, '花花', 19, '遼寧省大連市清河灣司馬道407路', '2019-12-22 00:00:00', '無', '2019-12-22 00:00:00', '2019-12-22 00:00:00', 0, '20200310');

3.1 建立數據庫對象類

用戶類

public class User {

    private Long id;
    private String name;
    private Integer age;
    private Date createTime;
    private Date updateTime;
    
    // ... get/set
}

學校類

public class School {

    private Long id;
    private String name;
    private String address;
    private Date createTime;
    private Date updateTime;  
  
    // ... get/set
}
  • 這兩個類都很是簡單,就是基本的數據庫信息。

3.2 建立DAO包

用戶Dao

public interface IUserDao {

     User queryUserInfoById(Long id);

}

學校Dao

public interface ISchoolDao {

    School querySchoolInfoById(Long treeId);

}

3.3 ORM配置文件

連接配置

<configuration>
    <environments default="development">
        <environment id="development">
            <transactionManager type="JDBC"/>
            <dataSource type="POOLED">
                <property name="driver" value="com.mysql.jdbc.Driver"/>
                <property name="url" value="jdbc:mysql://127.0.0.1:3306/itstack_demo_design?useUnicode=true"/>
                <property name="username" value="root"/>
                <property name="password" value="123456"/>
            </dataSource>
        </environment>
    </environments>

    <mappers>
        <mapper resource="mapper/User_Mapper.xml"/>
        <mapper resource="mapper/School_Mapper.xml"/>
    </mappers>

</configuration>
  • 這個配置與咱們日常使用的mybatis基本是同樣的,包括了數據庫的鏈接池信息以及須要引入的mapper映射文件。

操做配置(用戶)

<mapper namespace="org.itstack.demo.design.dao.IUserDao">

    <select id="queryUserInfoById" parameterType="java.lang.Long" resultType="org.itstack.demo.design.po.User">
        SELECT id, name, age, createTime, updateTime
        FROM user
        where id = #{id}
    </select>

    <select id="queryUserList" parameterType="org.itstack.demo.design.po.User" resultType="org.itstack.demo.design.po.User">
        SELECT id, name, age, createTime, updateTime
        FROM user
        where age = #{age}
    </select>

</mapper>

操做配置(學校)

<mapper namespace="org.itstack.demo.design.dao.ISchoolDao">

    <select id="querySchoolInfoById" resultType="org.itstack.demo.design.po.School">
        SELECT id, name, address, createTime, updateTime
        FROM school
        where id = #{id}
    </select>

</mapper>

3.4 單個結果查詢測試

@Test
public void test_queryUserInfoById() {
    String resource = "mybatis-config-datasource.xml";
    Reader reader;
    try {
        reader = Resources.getResourceAsReader(resource);
        SqlSessionFactory sqlMapper = new SqlSessionFactoryBuilder().build(reader);
        SqlSession session = sqlMapper.openSession();
        try {
            User user = session.selectOne("org.itstack.demo.design.dao.IUserDao.queryUserInfoById", 1L);
            logger.info("測試結果:{}", JSON.toJSONString(user));
        } finally {
            session.close();
            reader.close();
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
}
  • 這裏的使用方式和Mybatis是同樣的,都包括了;資源加載和解析、SqlSession工廠構建、開啓SqlSession以及最後執行查詢操做selectOne

測試結果

16:56:51.831 [main] INFO  org.itstack.demo.design.demo.ApiTest - 測試結果:{"age":18,"createTime":1576944000000,"id":1,"name":"水水","updateTime":1576944000000}

Process finished with exit code 0
  • 從結果上看已經知足了咱們的查詢需求。

3.5 集合結果查詢測試

@Test
public void test_queryUserList() {
    String resource = "mybatis-config-datasource.xml";
    Reader reader;
    try {
        reader = Resources.getResourceAsReader(resource);
        SqlSessionFactory sqlMapper = new SqlSessionFactoryBuilder().build(reader);
        SqlSession session = sqlMapper.openSession();
        try {
            User req = new User();
            req.setAge(18);
            List<User> userList = session.selectList("org.itstack.demo.design.dao.IUserDao.queryUserList", req);
            logger.info("測試結果:{}", JSON.toJSONString(userList));
        } finally {
            session.close();
            reader.close();
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
}
  • 這個測試內容與以上只是查詢方法有所不一樣;session.selectList,是查詢一個集合結果。

測試結果

16:58:13.963 [main] INFO  org.itstack.demo.design.demo.ApiTest - 測試結果:[{"age":18,"createTime":1576944000000,"id":1,"name":"水水","updateTime":1576944000000},{"age":18,"createTime":1576944000000,"id":2,"name":"豆豆","updateTime":1576944000000}]

Process finished with exit code 0
  • 測試驗證集合的結果也是正常的,目前位置測試所有經過。

7、總結

  • 以上經過中介者模式的設計思想咱們手寫了一個ORM框架,隱去了對數據庫操做的複雜度,讓外部的調用方能夠很是簡單的進行操做數據庫。這也是咱們日常使用的Mybatis的原型,在咱們平常的開發使用中,只須要按照配置便可很是簡單的操做數據庫。
  • 除了以上這種組件模式的開發外,還有服務接口的包裝也可使用中介者模式來實現。好比大家公司有不少的獎品接口須要在營銷活動中對接,那麼能夠把這些獎品接口統一收到中臺開發一個獎品中心,對外提供服務。這樣就不須要每個須要對接獎品的接口,都去找具體的提供者,而是找中臺服務便可。
  • 在上述的實現和測試使用中能夠看到,這種模式的設計知足了;單一職責開閉原則,也就符合了迪米特原則,即越少人知道越好。外部的人只須要按照需求進行調用,不須要知道具體的是如何實現的,複雜的一面已經有組件合做服務平臺處理。

8、推薦閱讀

相關文章
相關標籤/搜索