Java verwendet einen Reflexionsmechanismus, um ein einfaches ORM-Framework zu implementieren

Dieser Artikel hat an der Veranstaltung „Newcomer Creation Ceremony“ teilgenommen, um gemeinsam den Weg der Goldschöpfung zu beginnen.

Vor kurzem habe ich einem Freund geholfen, sein Kursdesign zu organisieren, das die Verwendung von jsp, servlet und mysql erfordert, um ein System zu implementieren.Wenn die Dao-Schicht nicht das Hibernate- oder mybatis-Framework verwendet, muss sie viel natives SQL schreiben, und Es muss auch mit ResultSet umgehen.Das Wichtigste ist, dass jede Funktion eine Dao-Datei schreiben muss, die viele Anweisungen mit ähnlichen Anforderungen enthält und ähnliche SELECT-, UPDATE-, DELETE-Anweisungen viele Male wiederholen kann.Das ist definitiv so kein Problem, so etwas als Aufgabe zu schreiben, aber es ist ein bisschen langweilig, erst kürzlich.

1. Namenskonventionen

Um das Problem zu vereinfachen, muss zunächst die Benennung von Tabellen und Entitätsklassen einer bestimmten Spezifikation entsprechen, ansonsten müssen Sie einige Konfigurationsannotationen für das Mapping schreiben, was die Vielseitigkeit erhöht, aber nicht erforderlich ist genügend. Hier werden die Namenskonventionen für Entitätsklassen und Tabellen beschrieben.

1.1 Tabellenname

Der Tabellenname verwendet Kleinbuchstaben und Unterstriche, und mehrere Wörter werden durch Unterstriche getrennt, z. B. user_group, wo die Felder auch so benannt werden.

1.2 Name der Entitätsklasse

Der Name der Entitätsklasse verwendet die Camel-Case-Nomenklatur und endet mit Entity. Der Name der Entitätsklasse bezieht sich auf den Namen der entsprechenden Tabelle, d. h. die Wörter werden durch Großbuchstaben getrennt. Beispiel: Die Entitätsklasse, die der Tabelle user_group entspricht, ist userGroupEntity, das Klassenmitglied Auch in Camel Case benannt.

2. Deklarieren Sie die abstrakte Elternklasse der Dao-Schicht

Hier deklariere ich eine abstrakte Elternklasse BaseDao mit Generika, und die Generika werden später als die entsprechende Entitätsklasse angegeben

public abstract class BaseDao<T> {}
复制代码

Der Vorteil der Verwendung von Generika besteht darin, dass der Name der Entitätsklasse durch Operationen wie Reflektion abgerufen werden kann und ein Klassenobjekt in der Klasse definiert werden kann, um die Entitätsklasse zu speichern und den Wert der Entitätsklasse im Konstruktor abzurufen.

private Class<T> entityClass;
    public BaseDao() {
        this.entityClass = null;
        Class<?> c = getClass();
        Type type = c.getGenericSuperclass();
        if (type instanceof ParameterizedType) {
            Type[] parameterizedType = ((ParameterizedType) type).getActualTypeArguments();
            this.entityClass = (Class<T>) parameterizedType[0];
        }
    }
复制代码

3. Hilfsmethoden schreiben

Gemäß der zuvor definierten Benennungsmethode müssen wir eine Methode schreiben, um den Variablennamen von Java in SQL umzuwandeln:

/**
     * 获取sql形式的变量名
     * @param str Java格式变量名
     * @return
     */
    public String getSqlFieldName(String str){
        for(int i=0;i<str.length();i++){
            if(i!=0 && str.charAt(i) > 'A' && str.charAt(i) < 'Z'){
                str = str.replace(""+str.charAt(i),"_" + (char)(str.charAt(i) - 'A' + 'a'));
            }
        }
        return str.toLowerCase();
    }
复制代码

Die Methode zum Abrufen des Tabellennamens der Entitätsklasse kann jetzt einfach über die Entitätsklasse und die Konvertierungsmethode durchgeführt werden:

/**
     * 获取表名
     * @return
     */
    private String getTableName(){
        String tableNames[] = entityClass.getTypeName().split("\\.");
        String tableName = tableNames[tableNames.length-1];
        tableName = tableName.substring(0,tableName.length()-6);
        tableName = getSqlFieldName(tableName);
        return tableName;
    }
复制代码

Implementieren Sie schließlich eine Methode, um die Datenbankverbindung zu erhalten:

/**获得数据库的连接,以进行其他操作
     *
     * @return 数据库连接
     */
    protected Connection getConnect(){
        Connection connection=null;
        try {
            Class.forName("com.mysql.jdbc.Driver");
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
        try {
            connection = DriverManager.getConnection(URL+DATEBASE,USERNAME,PASSWORD);
        } catch (SQLException e) {
            e.printStackTrace();
        }
        return connection;
    }
复制代码

4. Schreiben Sie eine Methode, die gemäß sql ausgeführt wird

这里的方法有三个,一个是直接执行无返回值的execute,一个查询返回一个对象queryOne,一个查询返回对象列表queryList,他们参数都有两个,一个是设置了占位符的字符串类型SQL语句,一个是Object类型的参数数组。 既然使用了占位符,肯定要使用PreparedStatement语句,这三个方法都需要给它设置参数,所以我抽象出一个方法获取设置好了参数的PreparedStatement对象,只需要根据参数的类型调用对应的set方法即可:

    protected PreparedStatement getPreparedStatement(Connection connection,String sql,Object [] params) throws SQLException {

        // 预准备语句
        PreparedStatement ps = connection.prepareStatement(sql);
        if(params == null)
            return ps;
        int index = 1;
        // 设置参数
        for(int i=0;i<params.length;i++){

            Object param = params[i];
            if(param == null)
                continue;
            if(param.getClass() == Integer.class){
                ps.setInt(index++, (Integer) param);
            }else if(param.getClass() == Double.class){
                ps.setDouble(index++, (Double) param);
            }else if(param.getClass() == String.class){
                ps.setString(index++, (String) param);
            }else if(param.getClass() == Long.class){
                ps.setLong(index++,(Long)param);
            }
        }
        return ps;
    }
复制代码

之后就可以实现execute方法了,只需要获取连接,获取预处理语句,最后关闭连接即可:

/**
     * 执行sql
     * @param sql SQL语句
     * @param params  参数
     * @throws SQLException
     */
    protected void execute(String sql,Object[] params) throws Exception {
        // 获取连接
        Connection connection = getConnect();
        // 设置准备语句
        PreparedStatement ps = getPreparedStatement(connection,sql,params);
        // 执行
        ps.execute();
        // 关闭
        connection.close();
    }
复制代码

接下来是两个略微复杂的查询方法,之所以查询,是因为要处理ResultSet,于是这里我实现了一个根据ResultSet直接自动填充获取实体类的方法:

/**
     * 根据result获取实体
     * @param resultSet
     * @return
     */
    public Object getEntity(ResultSet resultSet) throws IllegalAccessException, InstantiationException, SQLException {
        // 新建实体类
        Object object = entityClass.newInstance();
        // 获取成员变量
        Field fields[] = entityClass.getDeclaredFields();
        // 处理所有成员变量
        for(Field field:fields){
            // 设置可访问
            field.setAccessible(true);

            if(field.getType().equals(int.class)){
                // 设置int型变量
                field.setInt(object,resultSet.getInt(getSqlFieldName(field.getName())));
            }else if(field.getType().equals(String.class)){
                // 设置String型变量
                field.set(object,resultSet.getString(getSqlFieldName(field.getName())));
            }else if(field.getType().equals(double.class)){
                // 设置String型变量
                field.set(object,resultSet.getDouble(getSqlFieldName(field.getName())));
            }else if(field.getType().equals(Timestamp.class)){
                // 设置timestamp型变量
                field.set(object,resultSet.getTimestamp(getSqlFieldName(field.getName())));
            }else if(field.getType().equals(long.class)){
                field.set(object,resultSet.getLong(getSqlFieldName(field.getName())));
            }
        }
        return object;
    }
复制代码

有了这个方法查询一个对象就会变得简单不少,查询多个对象只需要解析多次获取多个实体类即可,于是可以实现两个query方法:

/**
     * 查询一个列表
     * @param sql SQL语句
     * @param params 参数列表
     * @return 返回结果
     * @throws SQLException
     */
    protected List<T> queryList(String sql,Object...params) throws Exception {
        // 获取连接
        Connection connection = getConnect();
        // 设置准备语句
        PreparedStatement ps = getPreparedStatement(connection,sql,params);
        // 获取结果集
        ResultSet res = ps.executeQuery();
        // 新建结果列表
        List<T> list = new ArrayList<>();
        while(res.next()){
            T t = (T) getEntity(res);
            list.add(t);
        }
        // 关闭
        connection.close();
        // 返回结果
        return list;
    }

    /**
     * 查询一条
     * @param sql SQL语句
     * @param params 参数列表
     * @return 执行结果
     * @throws SQLException
     */
    protected T queryOne(String sql,Object...params) throws Exception {
        // 获取连接
        Connection connection = getConnect();
        // 设置准备语句
        PreparedStatement ps = getPreparedStatement(connection,sql,params);
        // 获取结果集
        ResultSet res = ps.executeQuery();
        T t;
        if(res.next()){
            t = (T) getEntity(res);
        }else {
            t = null;
        }
        // 关闭
        connection.close();
        // 返回结果
        return t;
    }
复制代码

有了这些方法,我们便可以在子类中简化SQL的编写填充,此外后面的增删改查也是基于这些方法的。

5.编写实体类的增删查改方法

首先是增的save方法,这里我动态构造了SQL语句与参数列表,然后调用execute方法进行实际的执行,而构造的原理也是基于反射的:

   /**
     * 常规保存方式
     * @param t
     */
    public void save(T t) throws Exception{
        String tableName = getTableName();
        // 构造SQL语句
        StringBuilder sqlBuilder = new StringBuilder();
        sqlBuilder.append("INSERT INTO ").append(tableName).append("(");
        Field[] fields = entityClass.getDeclaredFields();
        Object params[] = new Object[fields.length];
        for(int i=0;i<fields.length;i++){
            fields[i].setAccessible(true);

            params[i] = fields[i].get(t);
        }
        for(int i=0;i<fields.length;i++){
            if(params[i] != null)
                sqlBuilder.append(getSqlFieldName(fields[i].getName())).append(",");
        }
        sqlBuilder.deleteCharAt(sqlBuilder.length()-1);
        sqlBuilder.append(")");
        sqlBuilder.append(" VALUES(");
        for(int i=0;i<fields.length;i++){
            if(params[i] != null)
                sqlBuilder.append("?,");
        }
        sqlBuilder.deleteCharAt(sqlBuilder.length()-1);
        sqlBuilder.append(")");


        execute(sqlBuilder.toString(),params);
    }
复制代码

删除和查询方法就更简单了,甚至不需要使用反射,只需要拿到主键的值即可,构造sql语句后分别调用execute方法和queryOne方法:

    /**
     * 根据主键获取某个对象
     * @param id 主键
     * @return
     * @throws Exception
     */
    public T get(Object id) throws Exception {
        String tableName = getTableName();
        String sql = "SELECT * FROM " + tableName + " WHERE id=?";
        return queryOne(sql,id);
    }
    
    /**
     * 删除表中某一个数据
     * @param id 主键
     */
    public void delete(Object id) throws Exception {
        String tableName = getTableName();
        StringBuilder sqlBuilder = new StringBuilder();
        sqlBuilder.append("DELETE FROM ").append(tableName).append(" WHERE id=?");
        Object[] params = {id};
        execute(sqlBuilder.toString(),params);
    }
复制代码

对于更改,本来我想直接传入实体类对象作为参数,但因为没有缓存的缘故,要么就把一个记录所有的字段都更新了,要么就查询找出发生变化的字段更新。但这两种的代价都太大,所以我决定传入发生变化成员-值映射字典进行更新,当然更新还需要指明主键,同样需要动态构造SQL语句:

    /**
     * 更新数据库中某一个对象
     * @param id 对象主键
     * @param paramMap 需要进行改变的成员-成员值映射
     */
    public void update(Object id, Map<String,Object> paramMap) throws Exception {
        String tableName = getTableName();
        // 构造SQL语句
        StringBuilder sqlBuilder = new StringBuilder();
        sqlBuilder.append("UPDATE ").append(tableName).append(" SET ");
        // 获取Map的键集合
        Set<String> set = paramMap.keySet();

        for(String key:set){
            sqlBuilder.append(key).append("=").append("?,");
        }
        sqlBuilder.deleteCharAt(sqlBuilder.length()-1);
        sqlBuilder.append(" WHERE id=?");

        Object params[] = new Object[set.size()+1];
        int index = 0;
        for(String key:set){
            params[index++] = paramMap.get(key);
        }
        params[index] = id;
        execute(sqlBuilder.toString(),params);
    }
复制代码

最后是一个多条记录的查询,利用lastId和length来实现分页:

/**
     * 获取下一页的对象列表
     * @param lastId
     * @param length
     */
    public List<T> getNextPage(Object lastId,int length) throws Exception {
        String tableName = getTableName();
        StringBuilder sqlBuilder = new StringBuilder().append("SELECT * FROM " + tableName + " WHERE id>? LIMIT ");
        sqlBuilder.append(length);
        Object params[] = {lastId};
        return queryList(sqlBuilder.toString(),params);
    }
复制代码

到这里,所有基本方法都已经实现了。

6.使用

BaseDao虽然很复杂,但是好处也很明显,就是子类基本上不需要增加什么方法就能实现大部分的业务,当然如果实在是查询过于复杂,也可以自己编写SQL语句,然后调用execute和query那三个方法来执行,但目前为止,还没有那么复杂的业务逻辑,所以基本上Dao层只需要继承一下BaseDao就可以交给Service层使用了:

public class EmployerDao extends BaseDao<EmployerEntity> {

    public static void main(String args[]){
        EmployerDao employerDao = new EmployerDao();
        EmployerEntity employerEntity = null;
        try {
            employerEntity = employerDao.get("zhang3");
        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println(employerEntity.getName());
    }
}
复制代码

运行情况 可见运行正常。

Supongo que te gusta

Origin juejin.im/post/7083728170343464968
Recomendado
Clasificación