This article is excerpted from Spring 5 core principles
1. Overview of implementation ideas
1.1 start with ResultSet
When it comes to ResultSet, the "little partner" with Java development experience is naturally the most familiar, but I believe it is also the "most familiar stranger" for most people. Everyone can get values from ResultSet, such as:
private static List<Member> select(String sql) { List<Member> result = new ArrayList<>(); Connection con = null; PreparedStatement pstm = null; ResultSet rs = null; try { //1. Load driver class Class.forName("com.mysql.jdbc.Driver"); //2. Establish connection con = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/gp-vip-spring-db-demo", "root","123456"); //3. Create statement set pstm = con.prepareStatement(sql); //4. Execute statement set rs = pstm.executeQuery(); while (rs.next()){ Member instance = new Member(); instance.setId(rs.getLong("id")); instance.setName(rs.getString("name")); instance.setAge(rs.getInt("age")); instance.setAddr(rs.getString("addr")); result.add(instance); } //5. Get result set }catch (Exception e){ e.printStackTrace(); } //6. Close result set, statement set and connection finally { try { rs.close(); pstm.close(); con.close(); }catch (Exception e){ e.printStackTrace(); } } return result; }
The above is our general operation before using the framework. With the increase of business and development, the frequency of repeated code in the data persistence layer is very high. Therefore, we thought of separating non functional code from business code. We first thought of separating the code logic of the encapsulated data of the ResultSet and adding a mapperRow() method to deal with the encapsulation of the results. The code is as follows:
private static List<Member> select(String sql) { List<Member> result = new ArrayList<>(); Connection con = null; PreparedStatement pstm = null; ResultSet rs = null; try { //1. Load driver class Class.forName("com.mysql.jdbc.Driver"); //2. Establish connection con = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/gp-vip-spring-db-demo", "root","123456"); //3. Create statement set pstm = con.prepareStatement(sql); //4. Execute statement set rs = pstm.executeQuery(); while (rs.next()){ Member instance = mapperRow(rs,rs.getRow()); result.add(instance); } //5. Get result set }catch (Exception e){ e.printStackTrace(); } //6. Close result set, statement set and connection finally { try { rs.close(); pstm.close(); con.close(); }catch (Exception e){ e.printStackTrace(); } } return result; } private static Member mapperRow(ResultSet rs, int i) throws Exception { Member instance = new Member(); instance.setId(rs.getLong("id")); instance.setName(rs.getString("name")); instance.setAge(rs.getInt("age")); instance.setAddr(rs.getString("addr")); return instance; }
However, in the real business scenario, the code logic repetition rate is too high. The above transformation can only apply the Member class, and the entity class has to be re encapsulated. Smart programmers will certainly not write a mapperRow() method for each entity class through pure manual labor, and will think of a code reuse scheme. We might as well make such a transformation. Create the Member class first:
package com.gupaoedu.vip.orm.demo.entity; import lombok.Data; import javax.persistence.Entity; import javax.persistence.Id; import javax.persistence.Table; import java.io.Serializable; @Entity @Table(name="t_member") @Data public class Member implements Serializable { @Id private Long id; private String name; private String addr; private Integer age; @Override public String toString() { return "Member{" + "id=" + id + ", name='" + name + '\'' + ", addr='" + addr + '\'' + ", age=" + age + '}'; } }
Optimize JDBC operations:
public static void main(String[] args) { Member condition = new Member(); condition.setName("Tom"); condition.setAge(19); List<?> result = select(condition); System.out.println(Arrays.toString(result.toArray())); } private static List<?> select(Object condition) { List<Object> result = new ArrayList<>(); Class<?> entityClass = condition.getClass(); Connection con = null; PreparedStatement pstm = null; ResultSet rs = null; try { //1. Load driver class Class.forName("com.mysql.jdbc.Driver"); //2. Establish connection con = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/gp-vip-spring-db-demo? characterEncoding=UTF-8&rewriteBatchedStatements=true","root","123456"); //Find property name based on class name Map<String,String> columnMapper = new HashMap<String,String>(); //Find field name based on property name Map<String,String> fieldMapper = new HashMap<String,String>(); Field[] fields = entityClass.getDeclaredFields(); for (Field field : fields) { field.setAccessible(true); String fieldName = field.getName(); if(field.isAnnotationPresent(Column.class)){ Column column = field.getAnnotation(Column.class); String columnName = column.name(); columnMapper.put(columnName,fieldName); fieldMapper.put(fieldName,columnName); }else { //The default is that the field name and attribute name are consistent columnMapper.put(fieldName, fieldName); fieldMapper.put(fieldName,fieldName); } } //3. Create statement set Table table = entityClass.getAnnotation(Table.class); String sql = "select * from " + table.name(); StringBuffer where = new StringBuffer(" where 1=1 "); for (Field field : fields) { Object value =field.get(condition); if(null != value){ if(String.class == field.getType()) { where.append(" and " + fieldMapper.get(field.getName()) + " = '" + value + "'"); }else{ where.append(" and " + fieldMapper.get(field.getName()) + " = " + value + ""); } //The others are not listed here. We will improve them later when we write the ORM framework } } System.out.println(sql + where.toString()); pstm = con.prepareStatement(sql + where.toString()); //4. Execute statement set rs = pstm.executeQuery(); //Metadata? //All additional information other than processing real values is saved int columnCounts = rs.getMetaData().getColumnCount(); while (rs.next()){ Object instance = entityClass.newInstance(); for (int i = 1; i <= columnCounts; i++) { //Entity class attribute name, corresponding to the field name of the database table //You can get all the fields of the entity class through the reflection mechanism //Get the class name under the current cursor from rs String columnName = rs.getMetaData().getColumnName(i); //It could be private Field field = entityClass.getDeclaredField(columnMapper.get(columnName)); field.setAccessible(true); field.set(instance,rs.getObject(columnName)); } result.add(instance); } //5. Get result set }catch (Exception e){ e.printStackTrace(); } //6. Close result set, statement set and connection finally { try { rs.close(); pstm.close(); con.close(); }catch (Exception e){ e.printStackTrace(); } } return result; }
The above skillfully uses the reflection mechanism to read the Class information and Annotation information, and associate, map and assign values to the columns in the database table and the fields in the Class, so as to reduce duplicate code.
1.2 why ORM framework is needed
Through the previous explanation, we have understood the basic implementation principle of ORM framework. ORM refers to object relational mapping (Object Relation Mapping) maps not only object values, but also relationships between objects, such as one to many, many to many, and one to one. There are many ORM frameworks on the market, including Hibernate, Spring JDBC, MyBatis, JPA, etc. here is a brief summary, as shown in the following table. | name | feature | description| |--------- --------- --------- Hibernate | automatic (block) | no need to write a sentence of SQL | MyBatis | semi automatic (block) | manual integration, support simple mapping, complex relationships need to write SQL | Spring JDBC | pure manual (block) | all SQL should be written by ourselves. It helps us design a set of standard processes|
Since there are so many choices on the market, why should I write my own ORM framework? This has to start with my experience as an architect in an airborne. The biggest problem faced by airborne is how to win the trust of the team "partners". At that time, there were only 8 people in the team, and the level of each person was uneven. Some people even had not been exposed to MySQL, such as Redis and other caching middleware, let alone. Basically, only Hibernate CRUD will be used, and the system performance has been affected. Due to the tight construction period, there was no time and energy to do systematic training for the team, and in order to give consideration to controllability, the idea of self-developed ORM framework came into being. I have made such a top-level design to reduce the interest saving cost of the team's "partners". The top-level interface has unified parameters and return values, as follows.
**(1) The interface model for specifying the query method is:**
/** * Get list * @param queryRule query criteria * @return */ List<T> select(QueryRule queryRule) throws Exception; /** * Get paging results * @param queryRule query criteria * @param pageNo Page number * @param pageSize Number of entries per page * @return */ Page<?> select(QueryRule queryRule,int pageNo,int pageSize) throws Exception; /** * Get list from SQL * @param sql SQL sentence * @param args parameter * @return */ List<Map<String,Object>> selectBySql(String sql, Object... args) throws Exception; /** * Get paging based on SQL * @param sql SQL sentence * @param pageNo Page number * @param pageSize Number of entries per page * @return */ Page<Map<String,Object>> selectBySqlToPage(String sql, Object [] param, int pageNo, int pageSize) throws Exception;
(2) The interface model specifying the deletion method is:
/** * Delete a record * @param entity entity The ID in cannot be empty. If the ID is empty and other conditions cannot be empty, they will not be executed * @return */ boolean delete(T entity) throws Exception; /** * Batch delete * @param list * @return Returns the number of rows affected * @throws Exception */ int deleteAll(List<T> list) throws Exception;
(3) The interface model specifying the insertion method is:
/** * Insert a record and return the inserted ID * @param entity Insert is performed as long as entity is not equal to null * @return */ PK insertAndReturnId(T entity) throws Exception; /** * Insert a record self increment ID * @param entity * @return * @throws Exception */ boolean insert(T entity) throws Exception; /** * Batch insert * @param list * @return Returns the number of rows affected * @throws Exception */ int insertAll(List<T> list) throws Exception;
(4) The interface model specifying the modification method is:
/** * Modify a record * @param entity entity The ID in cannot be empty. If the ID is empty and other conditions cannot be empty, they will not be executed * @return * @throws Exception */ boolean update(T entity) throws Exception;
Using this basic API, I later encapsulated a set based on Redis, MongoDB, ElasticSearch, Hive and HBase to reduce the learning cost of the team, greatly improve the controllability of the program and facilitate unified monitoring.
2. Build infrastructure
2.1 Page
The main purpose of defining the Page class is to provide top-level support for the unified return results of later paging queries. Its main functions include the encapsulation of paging logic and paging data.
package javax.core.common; import java.io.Serializable; import java.util.ArrayList; import java.util.List; /** * Paging object, including the current page data and paging information, such as the total number of records * It can support direct docking with JQuery EasyUI and BootStrap Table */ public class Page<T> implements Serializable { private static final long serialVersionUID = 1L; private static final int DEFAULT_PAGE_SIZE = 20; private int pageSize = DEFAULT_PAGE_SIZE; //Records per page private long start; //The position of the first data of the current page in the List, starting from 0 private List<T> rows; //The type of records stored in the current page is generally List private long total; //Total records /** * Construction method, only empty pages are constructed */ public Page() { this(0, 0, DEFAULT_PAGE_SIZE, new ArrayList<T>()); } /** * Default construction method * * @param start The starting position of this page data in the database * @param totalSize Total records in database * @param pageSize Capacity of this page * @param rows Data contained on this page */ public Page(long start, long totalSize, int pageSize, List<T> rows) { this.pageSize = pageSize; this.start = start; this.total = totalSize; this.rows = rows; } /** * Total records */ public long getTotal() { return this.total; } public void setTotal(long total) { this.total = total; } /** * Total pages taken */ public long getTotalPageCount() { if (total % pageSize == 0){ return total / pageSize; }else{ return total / pageSize + 1; } } /** * Data capacity per page */ public int getPageSize() { return pageSize; } /** * Get the record in the current page */ public List<T> getRows() { return rows; } public void setRows(List<T> rows) { this.rows = rows; } /** * Take the current page number of the page, starting from 1 */ public long getPageNo() { return start / pageSize + 1; } /** * Does this page have a next page */ public boolean hasNextPage() { return this.getPageNo() < this.getTotalPageCount() - 1; } /** * Does this page have a previous page */ public boolean hasPreviousPage() { return this.getPageNo() > 1; } /** * Gets the position of the first piece of data on any page in the dataset. The number of pieces per page uses the default value * * @see #getStartOfPage(int,int) */ protected static int getStartOfPage(int pageNo) { return getStartOfPage(pageNo, DEFAULT_PAGE_SIZE); } /** * Gets the position of the first piece of data on any page in the dataset * * @param pageNo Page number starting from 1 * @param pageSize Number of records per page * @return The first data on this page */ public static int getStartOfPage(int pageNo, int pageSize) { return (pageNo - 1) * pageSize; } }
2.2 ResultMsg
ResultMsg class is mainly the top-level design for unified return results, mainly including status code, result description content and return data.
package javax.core.common; import java.io.Serializable; //Bottom design public class ResultMsg<T> implements Serializable { private static final long serialVersionUID = 2635002588308355785L; private int status; //Status code, system return code private String msg; //Interpretation of status code private T data; //Put any result public ResultMsg() {} public ResultMsg(int status) { this.status = status; } public ResultMsg(int status, String msg) { this.status = status; this.msg = msg; } public ResultMsg(int status, T data) { this.status = status; this.data = data; } public ResultMsg(int status, String msg, T data) { this.status = status; this.msg = msg; this.data = data; } public int getStatus() { return status; } public void setStatus(int status) { this.status = status; } public String getMsg() { return msg; } public void setMsg(String msg) { this.msg = msg; } public T getData() { return data; } public void setData(T data) { this.data = data; } }
2.3 BaseDao
As the top-level interface of all BaseDao persistence frameworks, it mainly defines a unified parameter list and return value for addition, deletion, modification and query.
package javax.core.common.jdbc; import com.gupaoedu.vip.orm.framework.QueryRule; import javax.core.common.Page; import java.util.List; import java.util.Map; public interface BaseDao<T,PK> { /** * Get list * @param queryRule query criteria * @return */ List<T> select(QueryRule queryRule) throws Exception; /** * Get paging results * @param queryRule query criteria * @param pageNo Page number * @param pageSize Number of entries per page * @return */ Page<?> select(QueryRule queryRule,int pageNo,int pageSize) throws Exception; /** * Get list from SQL * @param sql SQL sentence * @param args parameter * @return */ List<Map<String,Object>> selectBySql(String sql, Object... args) throws Exception; /** * Get paging based on SQL * @param sql SQL sentence * @param pageNo Page number * @param pageSize Number of entries per page * @return */ Page<Map<String,Object>> selectBySqlToPage(String sql, Object [] param, int pageNo, int pageSize) throws Exception; /** * Delete a record * @param entity entity The ID in cannot be empty. If the ID is empty and other conditions cannot be empty, they will not be executed * @return */ boolean delete(T entity) throws Exception; /** * Batch delete * @param list * @return Returns the number of rows affected * @throws Exception */ int deleteAll(List<T> list) throws Exception; /** * Insert a record and return the inserted ID * @param entity The insert operation is performed as long as the entity is not equal to null * @return */ PK insertAndReturnId(T entity) throws Exception; /** * Insert a record self increment ID * @param entity * @return * @throws Exception */ boolean insert(T entity) throws Exception; /** * Batch insert * @param list * @return Returns the number of rows affected * @throws Exception */ int insertAll(List<T> list) throws Exception; /** * Modify a record * @param entity entity The ID in cannot be empty. If the ID is empty and other conditions cannot be empty, they will not be executed * @return * @throws Exception */ boolean update(T entity) throws Exception; }
2.4 QueryRule
If the QueryRule class is used to build query conditions, users do not need to write SQL when making conditional queries, so as to decouple business code from SQL.
package com.gupaoedu.vip.orm.framework; import java.io.Serializable; import java.util.ArrayList; import java.util.List; /** * QueryRule,The main function is to construct query criteria */ public final class QueryRule implements Serializable { private static final long serialVersionUID = 1L; public static final int ASC_ORDER = 101; public static final int DESC_ORDER = 102; public static final int LIKE = 1; public static final int IN = 2; public static final int NOTIN = 3; public static final int BETWEEN = 4; public static final int EQ = 5; public static final int NOTEQ = 6; public static final int GT = 7; public static final int GE = 8; public static final int LT = 9; public static final int LE = 10; public static final int ISNULL = 11; public static final int ISNOTNULL = 12; public static final int ISEMPTY = 13; public static final int ISNOTEMPTY = 14; public static final int AND = 201; public static final int OR = 202; private List<Rule> ruleList = new ArrayList<Rule>(); private List<QueryRule> queryRuleList = new ArrayList<QueryRule>(); private String propertyName; private QueryRule() {} private QueryRule(String propertyName) { this.propertyName = propertyName; } public static QueryRule getInstance() { return new QueryRule(); } /** * Add ascending rule * @param propertyName * @return */ public QueryRule addAscOrder(String propertyName) { this.ruleList.add(new Rule(ASC_ORDER, propertyName)); return this; } /** * Add descending rule * @param propertyName * @return */ public QueryRule addDescOrder(String propertyName) { this.ruleList.add(new Rule(DESC_ORDER, propertyName)); return this; } public QueryRule andIsNull(String propertyName) { this.ruleList.add(new Rule(ISNULL, propertyName).setAndOr(AND)); return this; } public QueryRule andIsNotNull(String propertyName) { this.ruleList.add(new Rule(ISNOTNULL, propertyName).setAndOr(AND)); return this; } public QueryRule andIsEmpty(String propertyName) { this.ruleList.add(new Rule(ISEMPTY, propertyName).setAndOr(AND)); return this; } public QueryRule andIsNotEmpty(String propertyName) { this.ruleList.add(new Rule(ISNOTEMPTY, propertyName).setAndOr(AND)); return this; } public QueryRule andLike(String propertyName, Object value) { this.ruleList.add(new Rule(LIKE, propertyName, new Object[] { value }).setAndOr(AND)); return this; } public QueryRule andEqual(String propertyName, Object value) { this.ruleList.add(new Rule(EQ, propertyName, new Object[] { value }).setAndOr(AND)); return this; } public QueryRule andBetween(String propertyName, Object... values) { this.ruleList.add(new Rule(BETWEEN, propertyName, values).setAndOr(AND)); return this; } public QueryRule andIn(String propertyName, List<Object> values) { this.ruleList.add(new Rule(IN, propertyName, new Object[] { values }).setAndOr(AND)); return this; } public QueryRule andIn(String propertyName, Object... values) { this.ruleList.add(new Rule(IN, propertyName, values).setAndOr(AND)); return this; } public QueryRule andNotIn(String propertyName, List<Object> values) { this.ruleList.add(new Rule(NOTIN, propertyName, new Object[] { values }).setAndOr(AND)); return this; } public QueryRule orNotIn(String propertyName, Object... values) { this.ruleList.add(new Rule(NOTIN, propertyName, values).setAndOr(OR)); return this; } public QueryRule andNotEqual(String propertyName, Object value) { this.ruleList.add(new Rule(NOTEQ, propertyName, new Object[] { value }).setAndOr(AND)); return this; } public QueryRule andGreaterThan(String propertyName, Object value) { this.ruleList.add(new Rule(GT, propertyName, new Object[] { value }).setAndOr(AND)); return this; } public QueryRule andGreaterEqual(String propertyName, Object value) { this.ruleList.add(new Rule(GE, propertyName, new Object[] { value }).setAndOr(AND)); return this; } public QueryRule andLessThan(String propertyName, Object value) { this.ruleList.add(new Rule(LT, propertyName, new Object[] { value }).setAndOr(AND)); return this; } public QueryRule andLessEqual(String propertyName, Object value) { this.ruleList.add(new Rule(LE, propertyName, new Object[] { value }).setAndOr(AND)); return this; } public QueryRule orIsNull(String propertyName) { this.ruleList.add(new Rule(ISNULL, propertyName).setAndOr(OR)); return this; } public QueryRule orIsNotNull(String propertyName) { this.ruleList.add(new Rule(ISNOTNULL, propertyName).setAndOr(OR)); return this; } public QueryRule orIsEmpty(String propertyName) { this.ruleList.add(new Rule(ISEMPTY, propertyName).setAndOr(OR)); return this; } public QueryRule orIsNotEmpty(String propertyName) { this.ruleList.add(new Rule(ISNOTEMPTY, propertyName).setAndOr(OR)); return this; } public QueryRule orLike(String propertyName, Object value) { this.ruleList.add(new Rule(LIKE, propertyName, new Object[] { value }).setAndOr(OR)); return this; } public QueryRule orEqual(String propertyName, Object value) { this.ruleList.add(new Rule(EQ, propertyName, new Object[] { value }).setAndOr(OR)); return this; } public QueryRule orBetween(String propertyName, Object... values) { this.ruleList.add(new Rule(BETWEEN, propertyName, values).setAndOr(OR)); return this; } public QueryRule orIn(String propertyName, List<Object> values) { this.ruleList.add(new Rule(IN, propertyName, new Object[] { values }).setAndOr(OR)); return this; } public QueryRule orIn(String propertyName, Object... values) { this.ruleList.add(new Rule(IN, propertyName, values).setAndOr(OR)); return this; } public QueryRule orNotEqual(String propertyName, Object value) { this.ruleList.add(new Rule(NOTEQ, propertyName, new Object[] { value }).setAndOr(OR)); return this; } public QueryRule orGreaterThan(String propertyName, Object value) { this.ruleList.add(new Rule(GT, propertyName, new Object[] { value }).setAndOr(OR)); return this; } public QueryRule orGreaterEqual(String propertyName, Object value) { this.ruleList.add(new Rule(GE, propertyName, new Object[] { value }).setAndOr(OR)); return this; } public QueryRule orLessThan(String propertyName, Object value) { this.ruleList.add(new Rule(LT, propertyName, new Object[] { value }).setAndOr(OR)); return this; } public QueryRule orLessEqual(String propertyName, Object value) { this.ruleList.add(new Rule(LE, propertyName, new Object[] { value }).setAndOr(OR)); return this; } public List<Rule> getRuleList() { return this.ruleList; } public List<QueryRule> getQueryRuleList() { return this.queryRuleList; } public String getPropertyName() { return this.propertyName; } protected class Rule implements Serializable { private static final long serialVersionUID = 1L; private int type; //Type of rule private String property_name; private Object[] values; private int andOr = AND; public Rule(int paramInt, String paramString) { this.property_name = paramString; this.type = paramInt; } public Rule(int paramInt, String paramString, Object[] paramArrayOfObject) { this.property_name = paramString; this.values = paramArrayOfObject; this.type = paramInt; } public Rule setAndOr(int andOr){ this.andOr = andOr; return this; } public int getAndOr(){ return this.andOr; } public Object[] getValues() { return this.values; } public int getType() { return this.type; } public String getPropertyName() { return this.property_name; } } }
2.5 Order
The Order class is mainly used to encapsulate sorting rules. The code is as follows:
package com.gupaoedu.vip.orm.framework; /** * SQL Sorting component */ public class Order { private boolean ascending; //Ascending or descending private String propertyName; //Which field is in ascending order and which field is in descending order public String toString() { return propertyName + ' ' + (ascending ? "asc" : "desc"); } /** * Constructor for Order. */ protected Order(String propertyName, boolean ascending) { this.propertyName = propertyName; this.ascending = ascending; } /** * Ascending order * * @param propertyName * @return Order */ public static Order asc(String propertyName) { return new Order(propertyName, true); } /** * Descending order * * @param propertyName * @return Order */ public static Order desc(String propertyName) { return new Order(propertyName, false); } }
Due to space reasons, the specific operation class will continue in the next article. Pay attention to WeChat official account Tom bomb structure and reply to "Spring" to get the complete source code.
This article is the original of "Tom bomb architecture". Please indicate the source for reprint. Technology lies in sharing, I share my happiness!
If this article is helpful to you, you are welcome to pay attention and praise; If you have any suggestions, you can also leave comments or private letters. Your support is the driving force for me to adhere to my creation. Focus on WeChat official account Tom structure, get more dry cargo!
It's not easy to be original. It's cool to insist. I've seen it here. Little partners remember to like, collect and watch it. Pay attention to it three times a button! If you think the content is too dry, you can share and forward it to your friends!