“This is the first day of my participation in the First Challenge 2022. For details: First Challenge 2022”

The database layer suggests the reason

I put forward the idea of database layer for the following reasons:

  1. The existing ORM frameworks in Java are not elegant enough to write, and neither Mybatis nor Spring-Data are particularly elegant.
  2. I have long wanted to implement an Orm framework similar to Laravel’s, which relies on point-by-point methods to implement basic SQL constructs.
  3. The table automatically generates various default ORM layer functions, and does not need to be implemented manually, only special methods need to be written manually.
  4. The presence of the database layer can reverse the rules of table building. For example, the database layer defines that there must be creator, creation time, update person, update time, delete person, delete time, etc. If you do not use these fields to build tables in the imported project, then you will get an error. In this way, as the reverse constraint of database table building, it will greatly reduce the cost of database layer detour, and also reduce the time spent by the database layer.
  5. The overall back-end architecture is loosely coupled, so that the business layer can focus on solving business problems, and the conversion of data in the database is exclusively entrusted to the database layer.

Architecture design

Architecture principles

A good architecture is necessary, and the implementation ideas here mainly refer to The Orm framework of SpringData and Laravel.

SpringData is compatible with a variety of different databases, which means that the architecture needs to include good extensibility, which requires the use of inheritance and polymorphism, which are implemented in Java through abstract classes and interfaces, respectively. At the same time, The default implementation of SpringData call basic methods can be used directly, which requires our database layer to provide basic database operation methods, even some complex but universal methods can also be provided as the default function.

One notable feature of Laravel’s Orm framework is its support for constructing Sql as a function, i.e

$existFolders = TFolder::where(['folder_name'= >$request['folder_name'].'delete_mark'= >false])->get();
Copy the code

This approach, which I personally find elegant, is converted to Java to implement methods similar to the following.

query.find("*"). The where (conditions). Orderby () page (). The size ();Copy the code

Through the dot – dot method to achieve the query.

To summarize, you need to implement a database layer with the following characteristics:

  • Support for extensions to different databases
  • Provides a large number of default methods
  • Provides basic methods, extended query methods, through the point-by-point way to achieve the creation of SQL
  • There are scenarios that can be used directly by the business layer as well as scenarios that can be used alone

In the implementation of point-by-point SQL creation, I refer to the implementation principle of Orm framework of Laravel. For each Query, it creates a Query as the main body of the Query, and uses different Query instances for the Query at the same time to avoid concurrency problems.

The final architecture is shown in mind maps as follows:



Defining base fields

First, you need to define an abstract class for the underlying query that implements some of the underlying methods, while providing the abstract methods that the subclasses themselves implement. So, what methods need to be defined? First, you need to be compatible with several basic SQL, which is CRUD related.

I’ve defined the following base fields here

public abstract class BaseQuery<T> {

    protected String primaryKey = "id";
    protected String table = "";
    protected List<String> with = new ArrayList<>();
    protected List<String> withCount = new ArrayList<>();
    protected Integer limit = -1;
    protected Integer offset = -1;
    protected List<String> traitInitializers = new ArrayList<>();
    protected List<String> globalScopes = new ArrayList<>();
    protected List<String> ignoreOnTouch = new ArrayList<>();
    protected List<String> columns = new ArrayList<>();
    protected WhereSyntaxTree wheres = new WhereSyntaxTree();
    protected Map<String, Object> params = new HashMap<>();

    protected Map<String, Object> updateSetMaps = new HashMap<>();

    protected List<String> orders = new ArrayList<>();
}
Copy the code

Among them, generic programming is used here, which needs to be added when inheriting the class. The T here refers to the business table itself, so when it comes to the final underlying class, it can be directly written.

Here, I define a recursive structure for where, because the WHERE statement is likely to be recursive, that is, when the WHERE statement is generated, there will probably be parentheses inside the parentheses. Its structure is as follows:

public class WhereSyntaxTree {

    public Boolean isFinal = false;
    List<WhereSyntaxTree> childTree = new ArrayList<>();
    public WhereSyntaxNode whereSyntaxNode;
}
Copy the code

As you can see, this where tree is a multi-fork tree. The content of the node is:

public class WhereSyntaxNode {
    private String name;
    private String operate = "=";
    private Object value;
    private String setName;
    private Boolean valueContainBracket;
    private Boolean listValueIsObject; // Whether the list data is a complex object, complex objects need special processing
}
Copy the code

Where a = :a and b = :b The setName in the node object is the variable name after the colon. By default, setName is equal to name, but when setName already appears in the previous node, it needs to regenerate the setName.

The BaseQuery here is essentially a basic query for a relational database implementation. Then consider implementing a database query, using PostgreSql as an example, implementing PostgreSQLBaseQuery, which inherits from BaseQuery. Its fields are defined as follows:

public class PostgreSQLBaseQuery<T> extends BaseQuery<T> implements LogExtenseInterface.TreeExtenseInterface.LRUCacheExtensionInterface {

    private Log logger = LogFactory.getLog(PostgreSQLBaseQuery.class);

    private Converter<String, String> camelToUnderscoreConverter = CaseFormat.LOWER_CAMEL.converterTo(CaseFormat.LOWER_UNDERSCORE);

    protected Class<T> clazz;

    public T model;
}
Copy the code

Some plugins are implemented here, in order to avoid writing too much code in BaseQuery, so that functions can be decouple, friends can follow their own understanding to implement log extension, cache extension and tree query extension.

Implement basic query related functions

First, each class of the underlying query needs a constructor, so let’s look at the implementation. BaseQuery is an abstract class and does not need to provide a constructor. PostgreSQLBaseQuery has introduced a generic class T that needs to be initialized, so its constructor is as follows:

public PostgreSQLBaseQuery(a) {
    Class clazz = getClass();
    while(clazz ! = Object.class) { Type t = clazz.getGenericSuperclass();if (t instanceof ParameterizedType) {
            Type[] args = ((ParameterizedType) t).getActualTypeArguments();
            if (args[0] instanceof Class) {
                this.clazz = (Class<T>) args[0];
                break; }}}try {
        Constructor constructor = this.clazz.getDeclaredConstructor();
        model = (T) constructor.newInstance();
    } catch (NoSuchMethodException e) {
        e.printStackTrace();
    } catch (IllegalAccessException e) {
        e.printStackTrace();
    } catch (InstantiationException e) {
        e.printStackTrace();
    } catch (InvocationTargetException e) {
        e.printStackTrace();
    }
    this.table = camelToUnderscoreConverter.convert(this.clazz.getSimpleName());
    logger.info(this.table);
}
Copy the code

The class of the currently instantiated object is retrieved for subsequent deserialization. When a HashMap transforms an object, you need to provide the object’s original class.

Then, to get the dot-dot effect, we need to implement a set of basic methods that must return the value of Query itself, because that’s the only way to keep dot-dot. The following methods are implemented in BaseQuery:

public BaseQuery<T> finds(List<String> names) {
    this.columns.addAll(names);
    return this;
}

public BaseQuery<T> size(Integer size) {
    this.limit = size;
    return this;
}

public BaseQuery<T> page(Integer page) {
    this.offset = page;
    return this;
}

public BaseQuery<T> orderBy(String key, String order) {
    this.orders.add(key + "" + order);
    return this;
}

public BaseQuery<T> find(String name) {
    this.columns.add(name);
    return this;
}

public AndWhereSyntaxTree defaultAndWheres(Map<String, Object> andWheres) {
    return wheres.createAndTree(andWheres);
}


public AndWhereSyntaxTree defaultAndWheresWithOperate(List<Triplet<String, String, Object>> andWheres) {
    return wheres.createAndTreeByOperate(andWheres);
}

public OrWhereSyntaxTree defaultOrWheresWithOperate(List<Triplet<String, String, Object>> orWheres) {
    return wheres.createOrTreeByOperate(orWheres);
}


public OrWhereSyntaxTree defaultOrWheres(Map<String, Object> orWheres) {
    return wheres.createOrTree(orWheres);
}

public BaseQuery<T> set(String name, Object value) {
    this.updateSetMaps.put(name, value);
    return this;
}

public BaseQuery<T> sets(Map<String, Object> sets) {
    this.updateSetMaps.putAll(sets);
    return this;
}
Copy the code

The above method provides the method required for basic query and update. By analyzing CRUD, it can be known that the current system requires logical deletion, so the deletion operation only needs to realize CRU, and RU has been provided at present. You just need to combine these methods in an inherited class to implement the requirements.

In addition to the provided methods, we need to define some abstract methods that subclasses must implement, as follows:

public abstract T simpleGet(a);

public abstract Long count(a);

public abstract List<Map<String, Object>> listMapGet();

public abstract Long insert(Map<String, Object> values);

public abstract Integer batchInsert(List<Map<String, Object>> listValues);

public abstract Integer update(Map<String, Object> conditions, Map<String, Object> values);

public abstract Integer update(List<Triplet<String, String, Object>> condition, Map<String, Object> values);

public abstract Integer updateById(Object primaryKey, Map<String, Object> value);
Copy the code

These abstract methods can be multiplied according to the associated default method requirements, which is an iterative process and won’t be delved into for now.

Finally, you can define default SQL generation methods in BaseQuery that subclasses can override or override.

  • The default generates functions that update the SQL
protected String defaultGenerateUpdateSql(a) {
    String sql = "";
    if (updateSetMaps.size() == 0) {
        return "";
    }
    sql = "UPDATE " + this.table + " SET ";
    String setSql = "";
    for (Map.Entry<String, Object> set : this.updateSetMaps.entrySet()) {
        if (setSql.equals("")) {
            setSql = set.getKey() + "=:set" + set.getKey();
            this.params.put("set" + set.getKey(), set.getValue());
        } else {
            setSql = setSql + "," + set.getKey() + "=:set" + set.getKey();
            this.params.put("set"+ set.getKey(), set.getValue()); }}if (StringUtils.hasText(setSql)) {
        sql = sql + "" + setSql;
    }
    String whereSql = this.wheres.getSql(this.params);
    if (StringUtils.hasText(whereSql)) {
        if (whereSql.startsWith("(") && whereSql.endsWith(")")) {
            whereSql = whereSql.substring(1, whereSql.length() - 1);
        }
        sql = sql + " WHERE " + whereSql + "";
    }
    sql = sql + ";";
    return sql;
}
Copy the code

Generate update statements directly from the current variable. This is why I mentioned earlier that each Query is a separate Query, and if it wasn’t, there would be conflicts and multithreading problems with variables. It’s a lot of trouble to solve.

  • Functions that generate queries for SQL by default
protected String defaultGenerateSql(a) {
    String sql = "";
    for (String column : this.columns) {
        if (sql.equals("")) {
            sql = "SELECT " + column;
        } else {
            sql = sql + ","+ column; }}if(! StringUtils.hasText(sql)) {return null;
    } else {
        sql = sql + "";
    }
    sql = sql + "FROM " + table + "";
    String whereSql = this.wheres.getSql(this.params);
    if (StringUtils.hasText(whereSql)) {
        if (whereSql.startsWith("(") && whereSql.endsWith(")")) {
            whereSql = whereSql.substring(1, whereSql.length() - 1);
        }
        sql = sql + "WHERE " + whereSql + "";
    }
    // TODO: 2021/8/17 order by
    if (this.orders.size() > 0) {
        String orderSql = "";
        for (String order : this.orders) {
            if (orderSql.equals("")) {
                orderSql = "order by " + order;
            } else {
                orderSql = orderSql + "," + order;
            }
        }
        sql = sql + orderSql + "";
    }
    // TODO: 2021/8/17 offset
    if (this.offset ! = -1) {
        if (this.limit ! = -1) {
            sql = sql + " offset " + (this.offset - 1) * this.limit + "";
        } else {
            sql = sql + " offset " + this.offset + ""; }}if (this.limit ! = -1) {
        sql = sql + " limit " + this.limit + "";
    }
    sql = sql + ";";
    return sql;
}
Copy the code

Well, that’s about it for our basic query.

How does a WHERE multi-fork tree generate SQL

If you remember, when we defined the WHERE of the base query class, it was a multi-fork tree type. So why is it a multi-fork tree? Let me illustrate it with a picture:



Then, the way to generate a full WHERE statement is obvious, simply printing out each node as a depth-first traversal of a multi-fork tree. The code is as follows:

public String getSql(Map<String, Object> params) {
    if (isFinal) {
        if (params.containsKey(whereSyntaxNode.getSetName())) {
            Random random = new Random();
            whereSyntaxNode.setSetName(MD5Utils.compMd5(whereSyntaxNode.getSetName() + LocalDateTime.now().toString() + random.ints().toString()));
        }
        params.put(whereSyntaxNode.getSetName(), whereSyntaxNode.getValue());
        if (whereSyntaxNode.getValueContainBracket()) {
            return whereSyntaxNode.getName() + "" + whereSyntaxNode.getOperate() + "(" + whereSyntaxNode.getSetName() + ")";
        } else {
            return whereSyntaxNode.getName() + "" + whereSyntaxNode.getOperate() + ":"+ whereSyntaxNode.getSetName(); }}else {
        String sunSql = "";
        for (WhereSyntaxTree whereSyntaxTree : childTree) {
            if (whereSyntaxTree instanceof AndWhereSyntaxTree) {
                if (sunSql.equals("")) {
                    sunSql = whereSyntaxTree.getSql(params);
                } else {
                    sunSql = sunSql + " AND "+ whereSyntaxTree.getSql(params); }}else if (whereSyntaxTree instanceof OrWhereSyntaxTree) {
                if (sunSql.equals("")) {
                    sunSql = whereSyntaxTree.getSql(params);
                } else {
                    sunSql = sunSql + " OR "+ whereSyntaxTree.getSql(params); }}}if (StringUtils.hasText(sunSql)) {
            return "(" + sunSql + ")";
        } else {
            return ""; }}}Copy the code

PostgreSql subquery class related functions

First, there are the abstract methods that need to implement the underlying query

@Override
public T simpleGet(a) {
    String sql = defaultGenerateSql();
    logger.info(sql);
    logger.info(this.params);
    try {
        T xx = SpringContextUtil.getBean(NamedParameterJdbcTemplate.class).queryForObject(sql, this.params, new BeanPropertyRowMapper<>(this.clazz));
        return xx;
    } catch (Exception e) {
    }
    return null;
}

@Override
public Long count(a) {
    String findStr = "count(" + primaryKey + ")";
    this.find(findStr);
    String sql = defaultGenerateSql();
    logger.info(sql);
    logger.info(this.params);
    Long ans = SpringContextUtil.getBean(NamedParameterJdbcTemplate.class).queryForObject(sql, this.params, Long.class);
    return ans;
}

@Override
public List<Map<String, Object>> listMapGet() {
    String sql = defaultGenerateSql();
    logger.info(sql);
    logger.info(this.params);
    return SpringContextUtil.getBean(NamedParameterJdbcTemplate.class).queryForList(sql, this.params);
}

@Override
public Long insert(Map<String, Object> values) {
    if (Objects.isNull(values)) values = new HashMap<>();
    values = removeNull(values);
    String names = "";
    String nameParams = "";
    for (Map.Entry<String, Object> tmp : values.entrySet()) {
        if (names.equals("")) {
            names = names + tmp.getKey();
            nameParams = nameParams + ":" + tmp.getKey();
        } else {
            names = names + "," + tmp.getKey();
            nameParams = nameParams + ",:"+ tmp.getKey(); }}this.params.putAll(values);
    params = convertParams(params);
    String sql = "INSERT INTO " + this.table + "(" + names + ") VALUES (" + nameParams + ") RETURNING " + primaryKey + ";";
    logger.info(sql);
    logger.info(this.params);
    Long id = SpringContextUtil.getBean(NamedParameterJdbcTemplate.class).queryForObject(sql, this.params, Long.class);
    return id;
}

@Override
public Integer batchInsert(List<Map<String, Object>> listValues) {
    if (Objects.isNull(listValues)) listValues = new ArrayList<>();
    int cnt = 0;
    String insertNames = "";
    List<String> insertNameParams = new ArrayList<>();
    int n = listValues.size();
    for (Map<String, Object> values : listValues) {
        values = removeNull(values);
        values = convertParams(values);
        String names = "";
        String nameParams = "";
        for (Map.Entry<String, Object> tmp : values.entrySet()) {
            if (names.equals("")) {
                names = names + tmp.getKey();
                nameParams = nameParams + ":" + tmp.getKey() + cnt;
            } else {
                names = names + "," + tmp.getKey();
                nameParams = nameParams + ",:" + tmp.getKey() + cnt;
            }
            this.params.put(tmp.getKey() + "" + cnt, tmp.getValue());
        }
        insertNames = names;
        nameParams = "(" + nameParams + ")";
        insertNameParams.add(nameParams);
        cnt++;
    }
    String sql = "INSERT INTO " + this.table + "(" + insertNames + ") VALUES " + ArrayStrUtil.slist2Str(insertNameParams, ",") + ";";
    logger.info(sql);
    logger.info(this.params);
    int x = SpringContextUtil.getBean(NamedParameterJdbcTemplate.class).update(sql, this.params);
    return x;
}


@Override
public Integer update(Map<String, Object> conditions, Map<String, Object> values) {
    if (Objects.isNull(conditions)) conditions = new HashMap<>();
    if (Objects.isNull(values)) values = new HashMap<>();
    conditions = removeNull(conditions);
    values = removeNull(values);
    this.updateSetMaps.putAll(values);
    WhereSyntaxTree whereSyntaxTree = defaultAndWheres(conditions);
    this.where(whereSyntaxTree);
    String sql = defaultGenerateUpdateSql();
    params = convertParams(params);
    logger.info(sql);
    logger.info(params);
    Integer influenceNumber = SpringContextUtil.getBean(NamedParameterJdbcTemplate.class).update(sql, this.params);
    return influenceNumber;
}

@Override
public Integer update(List<Triplet<String, String, Object>> condition, Map<String, Object> values) {
    if (Objects.isNull(condition)) condition = new ArrayList<>();
    if (Objects.isNull(values)) values = new HashMap<>();
    values = removeNull(values);
    this.updateSetMaps.putAll(values);
    WhereSyntaxTree whereSyntaxTree = defaultAndWheresWithOperate(condition);
    this.where(whereSyntaxTree);
    String sql = defaultGenerateUpdateSql();
    params = convertParams(params);
    logger.info(sql);
    logger.info(params);
    Integer influenceNumber = SpringContextUtil.getBean(NamedParameterJdbcTemplate.class).update(sql, this.params);
    return influenceNumber;
}


private Map<String, Object> convertParams(Map<String, Object> params) {
    Map<String, Object> newParams = new HashMap<>();
    for (Map.Entry<String, Object> param : params.entrySet()) {
        try {
            newParams.put(param.getKey(), DatetimeUtil.getLocalDatetimeByStr((String) param.getValue()));
        } catch(Exception e) { newParams.put(param.getKey(), param.getValue()); }}return newParams;
}

@Override
public Integer updateById(Object primaryKey, Map<String, Object> values) {
    Map<String, Object> conditions = new HashMap<>();
    conditions.put(this.primaryKey, primaryKey);
    return this.update(conditions, values);
}

@Override
public BaseQuery<T> where(WhereSyntaxTree whereSyntaxTree) {
    this.wheres = whereSyntaxTree;
    return this;
}
Copy the code

It then defines some complex queries to be invoked by business queries

/**
     * select * from a where id = 2 ......
     *
     * @param id
     * @return* /
public T findModelById(Object id) {
    return this.find("*").findById(id).simpleGet();
}

/**
     * select aa,aaa,aaaa from a where id = 2 ....
     *
     * @param id
     * @param fields
     * @return* /
protected T findModelById(Object id, List<String> fields) {
    return this.finds(fields).findById(id).simpleGet();
}

/**
     * select * from a where (x=1 and y=2) and delete_mark = false ......
     *
     * @param andCondition
     * @return* /
public T findModelBySimpleAnd(Map<String, Object> andCondition) {
    if (Objects.isNull(andCondition)) andCondition = new HashMap<>();
    andCondition = removeNull(andCondition);
    andCondition.put("deleted_mark".false);
    AndWhereSyntaxTree andWhereSyntaxTree = this.defaultAndWheres(andCondition);
    return this.find("*").where(andWhereSyntaxTree).orderBy(primaryKey, "desc").size(1).simpleGet();
}

/**
     * select * from a where (x=1 and y=2) and delete_mark = true ......
     *
     * @param andCondition
     * @return* /
public T findModelBySimpleAndDeletedMarkTrue(Map<String, Object> andCondition) {
    if (Objects.isNull(andCondition)) andCondition = new HashMap<>();
    andCondition = removeNull(andCondition);
    andCondition.put("deleted_mark".true);
    AndWhereSyntaxTree andWhereSyntaxTree = this.defaultAndWheres(andCondition);
    return this.find("*").where(andWhereSyntaxTree).orderBy(primaryKey, "desc").size(1).simpleGet();
}

/**
     * select * from a where (x = 1 or y = 2) and delete_mark = false .....
     *
     * @param orCondition
     * @return* /
public T findModelBySimpleOr(Map<String, Object> orCondition) {
    if (Objects.isNull(orCondition)) orCondition = new HashMap<>();
    orCondition = removeNull(orCondition);
    OrWhereSyntaxTree orWhereSyntaxTree = this.defaultOrWheres(orCondition);
    Map<String, Object> andWhereCondition = new HashMap<>();
    andWhereCondition.put("deleted_mark".false);
    andWhereCondition.put(MD5Utils.compMd5(orWhereSyntaxTree.toString() + LocalDateTime.now().toString()), orWhereSyntaxTree);
    AndWhereSyntaxTree andWhereSyntaxTree = this.defaultAndWheres(andWhereCondition);
    return this.find("*").where(andWhereSyntaxTree).size(1).simpleGet();
}
Copy the code

You can also define more methods for later subclasses to use.