I’m participating in nuggets Creators Camp # 4, click here to learn more and learn together!

preface

Now that you’ve learned about generics, annotations, and reflection (see my previous article if you’re not clear), it’s time to combine all three.

This article describes how I used annotations and reflection to implement a small ORM framework.

The requirements for a small ORM framework are as follows:

  1. There is a database connection pool and the ability to connect to the database (this is a basic requirement).
  2. Able to do data table fields andJavaAttribute automatic mapping of entity classes, regardless of subqueries and compound entity classes.
  3. There is basic CURD functionality (that is, delete, modify and check all by ID).
  4. support$and#To assemble in uncompiled and precompiled waysSQL

Database connection pool

  1. There is a database connection pool and the ability to connect to the database (this is a basic requirement).

First we need a database connection pool.

A database connection pool is similar to a thread pool in that it reduces repeated creation and destruction. A database connection pool is a database link, and a thread pool is a thread. The specific principle can go to see my design patterns column in the meta pattern, I think almost like this design.

In short, database connection pooling gives us a faster response and reduces performance and memory consumption.

The performance killer HiKariCP was chosen as the database connection pool for testing, but C3P0 or DBCP could also be used.

The use of HiKariCP

  1. Add dependencies in POM.xml.
<dependency> <groupId>com.zaxxer</groupId> <artifactId>HikariCP</artifactId> <version>2.5.1</version>Copy the code
  1. Create the hikari.properties file under the Resource folder.

This file name can be something else, but we can customize it later.

# address jdbcUrl = JDBC: database mysql: / / localhost: 3306 / test? UseSSL =false&useUnicode=true&characterEncoding=UTF-8 # Database Driver driverClassName= com.mysql.jdbc.driver # Database user name DataSource. Password =123456 dataSource. DatabaseName =test dataSource # maximum number of database link, when the database link fails, will rise to the number of the dataSource. MaximumPoolSize = 10Copy the code

Configure your database address and account password.

  1. test
public class HikariTest {
    public static void main(String[] args) {
        try (InputStream is = HikariTest.class.getClassLoader().getResourceAsStream("hikari.properties")) {
            // Load the properties file and parse it:
            Properties props = new Properties();
            props.load(is);
            HikariConfig config = new HikariConfig(props);
            HikariDataSource hikariDataSource = new HikariDataSource(config);
            Connection connection = hikariDataSource.getConnection();
            Statement statement = connection.createStatement();
            System.out.println();
        } catch(IOException | SQLException e) { e.printStackTrace(); }}}Copy the code

System.out.println(); If the connection is not null, the database connection pool is configured successfully.

Database connection pool factory

Although our HiKariCP database connection pool has been configured successfully, for ease of use, we need a factory to make it easier to get a link.

/** * public class ConnectFactory {private static DataSource = getHikariDataSource(); /** * @return */ private static DataSource getHikariDataSource() {HikariDataSource = null; Try (InputStream is = HikariTest. Class. GetClassLoader () getResourceAsStream (" hikari. Properties ")) {/ / loading the properties file and parse:  Properties props = new Properties(); props.load(is); HikariConfig config = new HikariConfig(props); hikariDataSource = new HikariDataSource(config); } catch (Exception e) { e.printStackTrace(); } return hikariDataSource; } @return public static Connection getConnection() {try {return dataSource. GetConnection (); } catch (SQLException e) { e.printStackTrace(); } return null; } /** * close the resource ** @param connection * @param statement * @param resultSet */ public static void close(connection) connection, Statement statement, ResultSet resultSet) { try { if (resultSet ! = null) { resultSet.close(); } if (statement ! = null) { statement.close(); } if (connection ! = null) { connection.close(); } } catch (SQLException e) { e.printStackTrace(); }}}Copy the code

annotations

  1. Able to do data table fields andJavaAttribute automatic mapping of entity classes, regardless of subqueries and compound entity classes.

We can use annotations to store table information and field information, we just need to configure the entity can be directly used to fetch the value of the annotation according to reflection, can be used to assemble a SQL section.

@DataTableName

/** * table annotation */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface DataTableName {
    String value(a);
}
Copy the code

@DataTableField

/** ** field annotation */
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DataTableField {
    String value(a);
}
Copy the code

@DataTablePkey

/** * primary key annotation */
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DataTablePkey {
}
Copy the code

I use ORACLE in the company, so I’m used to using PKEY as the primary key. You can also use ID, depending on your personal preference.

entity

User

/** * @datatablename ("T_USER") public class User {public User() {} public User(String name, Integer age, String address) { this.name = name; this.age = age; this.address = address; } public User(Long pkey, String name, Integer age, String address) { Pkey = pkey; this.name = name; this.age = age; this.address = address; } @DataTablePkey @DataTableField("PKEY") private Long Pkey; @DataTableField("NAME") private String name; @DataTableField("AGE") private Integer age; @DataTableField("ADDRESS") private String address; public String getName() { return name; } public User setName(String name) { this.name = name; return this; } public Integer getAge() { return age; } public User setAge(Integer age) { this.age = age; return this; } public String getAddress() { return address; } public User setAddress(String address) { this.address = address; return this; } public Long getPkey() { return Pkey; } public User setPkey(Long pkey) { Pkey = pkey; return this; } @Override public String toString() { return "User{" + "Pkey=" + Pkey + ", name='" + name + ''' + ", age=" + age + ", address='" + address + ''' + '}'; }}Copy the code

Realize the CURD

  1. Has basic CURD functionality.

This part is to get the values in the annotations and then assemble the SQL.

As for assembling SQL with # and $, you may need to use annotation + dynamic proxy. This will need to be done later. For now, we will step out of CURD.

Create a BaseDao

Create an interface called IBaseDao and an abstract class called BaseDao. In order to obtain an Entity class object, write a BaseDao abstract class that all DAOs can inherit. The BaseDao abstract class contains the common base CURD method abstract class. Our CURD is basically implemented in the BaseDao abstract class.

BaseDao

private Class<T> clazz;
{
    clazz= (Class<T>) ((ParameterizedType)this.getClass().getGenericSuperclass()).getActualTypeArguments()[0];
}
Copy the code

Since the UserDao we created inherits from BaseDao, our generics are written on the parent class, so we need to get the class object of the parent class.

GetGenericSuperclass () and getSuperclass() are both class objects that get the parent of the current object, but the former does not erase generics while the latter does.

In order to get the generic class, the first thing to think about is new T()? Obviously not. Generics are erased after compilation, so BaseDao

will become an Object after compilation, so you must create BaseDao

as an abstract class and let a Dao inherit it.

There are UserDao

public class UserDao extends BaseDao<User> {}Copy the code

This is the UserDao, which you will see again for a long time.

Delete function

Let’s implement a delete by PKEY function. It is also possible to delete data by an object, but the first goal is to implement a framework with basic CURD. We will dig a hole here and come back to it later.

Create interface methods

IBaseDao

public interface IBaseDao<T>{
     int deleteByPkey(long PKEY);
}
Copy the code

Implement interface methods

Here, just implement the delete method in BaseDao.

I’ve written about a hundred lines of code here.

INTEGER_STR
LONG_STR
STRING_STR
DATE_STR
Copy the code

This several types, is more commonly used types, because the Java switch incredibly don’t support by Integer. Class. GetSimpleName () to obtain the string? ! I had to write a string myself, which was pretty silly, to set the parameters in my preparedStatement.

switch (name){ case INTEGER_STR: preparedStatement.setInt(i,Integer.valueOf(String.valueOf(value))); break; case LONG_STR: preparedStatement.setLong(i,Long.valueOf(String.valueOf(value))); break; case STRING_STR: preparedStatement.setString(i,String.valueOf(value)); break; Case DATE_STR: / / may be an error preparedStatement. SetDate (I, (Date), value); break;

Later found, can be directly preparedStatement setObject (I, value); If you can set the Object type directly, why do you have so many types? ! So I look at the method of setObject, and this method helps us determine whether we should actually call setObject or setString, which means that when we’re not sure what the type of an argument is, we just call setObject, and then we just leave it to setObject to determine the type of the argument.

ExecuteUpdate methods is intended to perform set parameters and execution of SQL preparedStatement. ExecuteUpdate ().

I extracted the table name, and then I thought I should extract the mapping between the attributes of the entity class and the fields of the database, so that I basically don’t need to touch the annotation and class in the following code, because the data we need is the value and attribute names of the annotation.

And I also added entityFieldToDataTableFieldMap and dataTableFieldToEntityFieldMap, the former is the entity class attribute corresponding data table fields, which is the data table fields corresponding to the entity class attribute, in order to more convenient, I’ve also drawn an entityFieldToFieldMap for the entity-class attribute Field.

The setParameters method was originally intended to use paramSequence to store all? The placeholders correspond to any parameter, and paramMap stores the values of all parameters so that the values of the parameters correspond to the placeholders.

Friends do not need to worry, I will finally put my edited source code on gitee, and then put the link address in the last, interested friends can download to play.

public abstract class BaseDao<T> implements IBaseDao<T> { private Class<T> clazz; private final Map<String,String> entityFieldToDataTableFieldMap=new HashMap<>(); private final Map<String,String> dataTableFieldToEntityFieldMap=new HashMap<>(); private final Map<String,Field> entityFieldToFieldMap=new HashMap<>(); { clazz= (Class<T>) ((ParameterizedType)this.getClass().getGenericSuperclass()).getActualTypeArguments()[0]; Field[] fields = clazz.getDeclaredFields(); for (Field field : fields) { DataTableField fieldAnnotation = field.getAnnotation(DataTableField.class); String entityField=field.getName(); String dataTableField=field.getName(); if (fieldAnnotation! =null){ dataTableField=fieldAnnotation.value(); } entityFieldToDataTableFieldMap.put(entityField,dataTableField); dataTableFieldToEntityFieldMap.put(dataTableField,entityField); entityFieldToFieldMap.put(entityField,field); }} / temporary support only this several types of * * * * wanted to call it a direct access to an Integer. The getClass (). The getName (), but the switch does not support, can't help it. */ private final static String INTEGER_STR="Integer"; private final static String LONG_STR="Long"; private final static String STRING_STR="String"; private final static String DATE_STR="Date"; private final String tableName = getTableName(); Private Final String tablePkeyFieldName=getTablePkeyFieldName(); @override public int deleteByPkey(long PKEY) {StringBuilder Builder =new StringBuilder("DELETE FROM "); builder.append(tableName).append(" WHERE "); builder.append(tablePkeyFieldName); builder.append("=?" ); List<String> paramSequence=new ArrayList<>(1); paramSequence.add(tablePkeyFieldName); Map<String,Object> paramMap=new HashMap<>(2); paramMap.put(tablePkeyFieldName,PKEY); return executeUpdate(builder.toString(),paramSequence,paramMap); } private String getTablePkeyFieldName() { String pkey=null; Field[] fields = clazz.getDeclaredFields(); for (Field item:fields) { DataTablePkey pkeyA = item.getAnnotation(DataTablePkey.class); if (pkeyA! =null){ pkey=item.getAnnotation(DataTableField.class).value(); break; }} if (pkey==null){pkey=" pkey "; } return pkey; } @return */ private String getTableName() {String tableName; DataTableName tableAnnotation = clazz.getAnnotation(DataTableName.class); If (tableAnnotation==null){// When there is no Table annotation tableName= clazz.getSimplename (); }else { tableName=tableAnnotation.value(); } return tableName; } / * * * to perform a preparedStatement. ExecuteUpdate () * @ param SQL * @ param paramSequence the name of the stored data, The sequence of data names must be the same as that of parameters to be inserted * @param paramMap Stores parameter values in paramSequence * @return */ private int executeUpdate(String SQL, List<String> paramSequence, Map<String, Object> paramMap) { Connection connection= ConnectFactory.getConnection(); try { PreparedStatement preparedStatement =connection.prepareStatement(sql); setParameters(paramSequence, paramMap, preparedStatement); return preparedStatement.executeUpdate(); } catch (SQLException e) { e.printStackTrace(); }finally { ConnectFactory.close(connection,null,null); } return 0; } /** * set parameter * @param paramSequence specifies the name of the data to be stored. The sequence of data names must be the same as that of parameters to be inserted * @param paramMap Stores parameter values in paramSequence * @Param preparedStatement * @throws SQLException */ private void setParameters(List<String> paramSequence, Map<String, Object> paramMap, PreparedStatement preparedStatement) throws SQLException { for (int i = 0; i < paramSequence.size();) { String key = paramSequence.get(i); i++; Object value = paramMap.get(key); preparedStatement.setObject(i,value); }}}Copy the code

Only the new code will be posted later.

Query function

After I wrote the delete method, I realized that I was tinkering with it all the time, which is definitely not good programming, so I decided to get my thoughts straight before I started writing.

SELECT * FROM TABLE_NAME WHERE PKEY=? .

Here we are using only primary key query, you can see that we need to use the following data: indicate, primary key.

I thought about it, query based on primary key is actually the same as query all, conditional query is no different, they are executing SQL, return a ResultSet.

In other words, we need to do three actions:

  1. Assemble the SQL
  2. Set the parameters
  3. Processing result set

To set the parameters, we already have the setParameters method. We just need to assemble the paramSequence and paramMap according to the rules.

To create a SQL statement, you only need to create a WHERE statement after the WHERE statement, which is actually quite simple, so you can use an entity class as a parameter, and directly add the attributes of the entity class to the WHERE statement. SELECT * FROM TABLE_NAME WHERE PKEY=? AND NAME=? We must solve the problem of “AND”. In order to improve efficiency, we must not use the method of 1=1 to solve the problem of excess “AND”, so we can add a flag here to determine whether the current parameter is the first one. If yes, we do not add “AND”, AND if not, we add “AND”.

If you’re dealing with a result set, we’ll always return a List

set.

We use get and set methods when we assemble SQL and when we process result sets. Here’s a method that assembles the name of the method, and then calls the method with incoming arguments through reflection. We can get parameters, and set parameters as well.

Create interface methods

IBaseDao

List<T> select(T t);
Copy the code

Implement interface methods

BaseDao

SQL StringBuilder Builder = new StringBuilder(" select * FROM "+ tableName);  List<String> paramSequence = new ArrayList<>(); Map<String, Object> paramMap = new HashMap<>(); if (t ! = null) { builder.append(" WHERE "); / / here we use entityFieldToFieldMap to traverse the Set < String > keySet. = entityFieldToFieldMap keySet (); Iterator<String> iterator = keySet.iterator(); Boolean isNoFirst=false; try { while (iterator.hasNext()) { String entityFieldName = iterator.next(); String getMethodName = getGetOrSetMethodName(entityFieldName, true); Method method = clazz.getDeclaredMethod(getMethodName); Object value = method.invoke(t); if (value == null) { continue; } / / when the value is not null, began to deal with String dataFieldName = entityFieldToDataTableFieldMap. Get (entityFieldName); if (isNoFirst){ builder.append(" AND "); }else {AND isNoFirst=true; } builder.append(dataFieldName).append("=?" ); paramSequence.add(entityFieldName); paramMap.put(entityFieldName, value); } } catch (Exception e) { e.printStackTrace(); } } return executeQuery(builder.toString(), paramSequence, paramMap); } / * * * to perform a preparedStatement. ExecuteQuery () SQL * * * @ param @ param paramSequence * @ param paramMap * @ return * / private  List<T> executeQuery(String sql, List<String> paramSequence, Map<String, Object> paramMap) { Connection connection = ConnectFactory.getConnection(); PreparedStatement preparedStatement = null; ResultSet resultSet = null; List<T> result = new ArrayList<>(); try { preparedStatement = connection.prepareStatement(sql); setParameters(paramSequence, paramMap, preparedStatement); / / execute SQL resultSet = preparedStatement. ExecuteQuery (); / / processing while the result Set (resultSet. Next ()) {Set < String > keySet. = dataTableFieldToEntityFieldMap keySet (); Iterator<String> iterator = keySet.iterator(); T t = clazz.newInstance(); for (int i = 0; iterator.hasNext(); i++) { String dataFieldName = iterator.next(); Object Value = resultSet. GetObject (dataFieldName); String entityFieldName = dataTableFieldToEntityFieldMap.get(dataFieldName); String setMethodName = getGetOrSetMethodName(entityFieldName, false); Method method = clazz.getDeclaredMethod(setMethodName, entityFieldToFieldMap.get(entityFieldName).getType()); // Invoke method. Invoke (t, value) by method reflection; } result.add(t); } } catch (Exception e) { e.printStackTrace(); } finally { ConnectFactory.close(connection, preparedStatement, resultSet); } return result; }Copy the code

The implementation of the select method exceeded my expectations, originally only wanted to implement the query by ID, then thought that simple can go to practice according to the entity class to obtain the parameter query, and then because the parameter may be NULL, I realized the query all data.

Hahaha, it seems that thinking too much will feel difficult, or to do it, in order to encounter real problems, and then to solve it, and should not be imaginary problems.

New features

INSERT INTO TABLE_NAME(FIELD1,FIELD2) VALUES(VALUE1,VALUE2)

The data we need are: representation, database field names, entity objects.

The specific approach is: We need to get the value of the attribute in the incoming entity object. If it is not empty, we add the corresponding database field name to the SQL. We need two strings to assemble it: fieldString and valueString, which is used to concatenate placeholders. We only need to cut off the last bit of the string.

Also assemble the parameters into our paramSequence and paramMap.

Oh, there’s a detail need to implement, is after the insert to the new primary key to return to, the premise is that we’re going to the primary key in the data table on the open, we need to call a preparedStatement. ExecuteUpdate (after), To invoke a preparedStatement. GetGeneratedKeys () to obtain the primary key of the new.

Considering the new function is not to need to use the primary key of the entity class, I was in a new noPkeyEntityFieldToDataTableFieldMap attributes to hold the primary key, so don’t need always to judge when use now is the primary key. Add code as shown below.

Tweaked the previous code a little bit.

Create interface methods

IBaseDao

Long insert(T t);
Copy the code

Implement interface methods

BaseDao

@override public Long StringBuilder fieldBuilder = new StringBuilder(" insert INTO "+ tableName + "("); StringBuilder valueBuilder = new StringBuilder("VALUES("); List<String> paramSequence = new ArrayList<>(); Map<String, Object> paramMap = new HashMap<>(); if (t == null) { return null; } / / here we use dataTableFieldToEntityFieldMap to traverse the Set < String > keySet. = noPkeyEntityFieldToDataTableFieldMap keySet (); Iterator<String> iterator = keySet.iterator(); Try {while (iterator.hasnext ()) {dataFieldName String entityFieldName = iterator.next(); String dataFieldName = noPkeyEntityFieldToDataTableFieldMap.get(entityFieldName); String getMethodName = getGetOrSetMethodName(entityFieldName, true); Method method = clazz.getDeclaredMethod(getMethodName); Object value = method.invoke(t); if (value == null) { continue; Fieldbuilder.append (dataFieldName).append(","); valueBuilder.append("? , "); paramSequence.add(entityFieldName); paramMap.put(entityFieldName, value); } fieldBuilder.deleteCharAt(fieldBuilder.length()-1).append(") "); valueBuilder.deleteCharAt(valueBuilder.length()-1).append(")"); } catch (Exception e) { e.printStackTrace(); } fieldBuilder.append(valueBuilder.toString()); return executeInsert(fieldBuilder.toString(),paramSequence,paramMap); } / * * * to perform a preparedStatement. ExecuteUpdate (), Param SQL * @param paramSequence * @param paramMap * @return */ private Long executeInsert(String SQL, List<String> paramSequence, Map<String, Object> paramMap) { Connection connection = ConnectFactory.getConnection(); PreparedStatement preparedStatement =null; ResultSet resultSet=null; Try {/ / new String [] {tablePkeyFieldName} is set to return to the primary key of the preparedStatement = connection. The prepareStatement (SQL, new String [] {tablePkeyFieldName}); setParameters(paramSequence, paramMap, preparedStatement); int update = preparedStatement.executeUpdate(); if (update>0){ resultSet= preparedStatement.getGeneratedKeys(); resultSet.next(); return (Long) resultSet.getObject(1); } } catch (SQLException e) { e.printStackTrace(); } finally { ConnectFactory.close(connection, preparedStatement, resultSet); } return null; }Copy the code

The executeInsert method here is very similar to the executeUpdate method, but it takes an extra step and returns the new primary key, so there’s one more method, and you’ll have a chance to see how to combine the two.

Modify the function

There is a final modification method, can see here is true love, I hope you true love can dot this 👍, received 👍 I will be more motivated to bring you better articles.

UPDATE TABLE_NAME SET FIELD1=VALUE1,FIELD2=VALUE2 WHERE PKEY=VALUE3

The data we need are: table names, database field names, entity objects.

The way you concatenate SQL here is almost the same as a query, but slightly different.

Query: SELECT * FROM TABLE_NAME WHERE FIELD1=VALUE1 AND FIELD2=VALUE2

UPDATE TABLE_NAME SET FIELD1=VALUE1,FIELD2=VALUE2 WHERE PKEY=VALUE3

The WHERE substatement of the query is similar to the SET substatement of the modification

Isn’t it just the difference in delimiters, Ger? Kind of feels like dynamic SQL.

But if you want to split this step into one method, AND you want to solve the problem of “AND” /”,” AND you want to use a public, you definitely need to use a queue to store the fields, so let’s just write the original logic.

Create interface methods

IBaseDao

int updateByPkey(T t);
Copy the code

Implement interface methods

BaseDao

@Override public int updateByPkey(T t) { String pkeyFieldName = dataTableFieldToEntityFieldMap.get(tablePkeyFieldName); String getPkeyMethodName = getGetOrSetMethodName(pkeyFieldName, true); Object pkey = null; try { Method getPkeyMethod = clazz.getDeclaredMethod(getPkeyMethodName); pkey = getPkeyMethod.invoke(t); } catch (Exception e) { e.printStackTrace(); } the if (t = = null | | pkey = = null) {/ / parameter is null, the pkey is empty don't deal with the return 0; SQL StringBuilder Builder = new StringBuilder("UPDATE "+ tableName+" SET "); List<String> paramSequence = new ArrayList<>(); Map<String, Object> paramMap = new HashMap<>(); / / here we use noPkeyEntityFieldToDataTableFieldMap to traverse the Set < String > keySet. = noPkeyEntityFieldToDataTableFieldMap keySet (); Iterator<String> iterator = keySet.iterator(); Boolean isNoFirst = false; try { while (iterator.hasNext()) { String entityFieldName = iterator.next(); String getMethodName = getGetOrSetMethodName(entityFieldName, true); Method method = clazz.getDeclaredMethod(getMethodName); Object value = method.invoke(t); if (value == null) { continue; } / / when the value is not null, began to deal with String dataFieldName = noPkeyEntityFieldToDataTableFieldMap. Get (entityFieldName); if (isNoFirst) { builder.append(","); } else {// The first time is not increment, isNoFirst = true; } builder.append(dataFieldName).append("=?" ); paramSequence.add(entityFieldName); paramMap.put(entityFieldName, value); } builder.append(" WHERE "); builder.append(tablePkeyFieldName).append("=?" ); paramSequence.add(tablePkeyFieldName); paramMap.put(tablePkeyFieldName, pkey); } catch (Exception e) { e.printStackTrace(); } return executeUpdate(builder.toString(), paramSequence, paramMap); }Copy the code

Here the CURD function is finished, simple add, delete, change and check the scene can be used.

CURD function test

Although I am a side development side test, basically there is no problem drop, but still want to give you an explanation, here or put the test results.

I wrote some data in the T_USER table.

Test code:

public static void main(String[] args) { UserDao userDao = new UserDao(); List<User> select1 = userDao.select(null); System.out.println("-----select1-----"); for (User user : select1) { System.out.println(user.toString()); } int update1 = userDao.deleteByPkey(2L); System. The out. Println (" update1: "+ update1); List<User> select2 = userDao.select(null); System.out.println("-----select2-----"); for (User user : select2) { System.out.println(user.toString()); } Long pkey = userDao. Insert (new User("小 gang ", 18, "小 gang ")); System. The out. Println (" the pkey: "+ pkey); List<User> select3 = userDao.select(new User().setPkey(pkey)); System.out.println("-----select3-----"); for (User user : select3) { System.out.println(user.toString()); } int update2 = userDao. UpdateByPkey (new User(3L, "小蓝", 99, "小蓝")); System. Out. Println (" out: "+ out); List<User> select4 = userDao.select(new User().setPkey(3L)); System.out.println("-----select4-----"); for (User user : select4) { System.out.println(user.toString()); }}Copy the code

Execute!

—–select1—–

User{Pkey=1, name=’ xiaoming ‘, age=18, address=’ xiaoming ‘}

User{Pkey=2, name=’小 blue ‘, age=99, address=’ 小 blue ‘}

User{Pkey=3, name=’小红’, age=18, address=’ 小红’}

User{Pkey=4, name=’小 gang ‘, age=18, address=’ 小 gang ‘}

Update1:1.

—–select2—–

User{Pkey=1, name=’ xiaoming ‘, age=18, address=’ xiaoming ‘}

User{Pkey=3, name=’小红’, age=18, address=’ 小红’}

User{Pkey=4, name=’小 gang ‘, age=18, address=’ 小 gang ‘}

The pkey: 9

—–select3—–

User{Pkey=9, name=’小 gang ‘, age=18, address=’ 小 gang ‘}

Out: 1.

—–select4—–

User{Pkey=3, name=’小 blue ‘, age=99, address=’ 小 blue ‘}

Attention! The integer type of the entity class must be changed to wrapper type, otherwise it will have a default value and affect our query.

This is the end of the basic functionality, custom SQL still needs to be thought of, will definitely use dynamic proxy, dynamic SQL may have to wait a little later to implement.

The road should be eaten mouthful by mouthful, the meal should be walked step by step. Let’s move on to our next goal: custom SQL.

Gitee address: ORM

Custom SQL

  1. support$and#To assemble in uncompiled and precompiled waysSQL

This section explains how to implement custom SQL.

One quick question

Problem description

Here’s a slight problem (I almost gave up) :

Generic classes are available for objects created by new, but not for cglib dynamic proxies or Java native dynamic proxies.

Let’s take UserDao as an example.

This is created by the new object, you can see the name here is org. Example, simulate the orm. Your. The entity. The User, which is the access to the current generic class object.

Next comes the proxy object created by Cglib.

First, we need a generator for the proxy object. I’ve written a simple one here, but I won’t post the code, as shown below.

Calling code:

UserDao instance = (UserDao)CglibDynamicProxy.getProxyInstance(UserDao.class);
instance.insert(null);
Copy the code

Next, let’s see if we can get a generic class object here.

Obviously, it can’t get it. It shows a cast error.

Problem solution

Why is this happening?

That’s because we’re using a dynamic proxy, which generates a subclass that inherits the UserDao. We can’t get the generic class object from the UserDao, so we need to get the BaseDao one level higher to get the generic class object.

I also can make a conclusion according to the phenomenon, hope to have more professional big guy to answer.

So we need to modify the code in BaseDao:

{
    clazz = (Class<T>) ((ParameterizedType) this.getClass().getGenericSuperclass()).getActualTypeArguments()[0];
}
Copy the code

to

{
    // Get the inherited parent (without erasing generics)
    Type type = this.getClass().getGenericSuperclass();
    if(type instanceof ParameterizedType){
        // The current class inherits ParameterizedType and can get generic types directly
        clazz = (Class<T>) ((ParameterizedType) type).getActualTypeArguments()[0];
    }else {
        // You can't get generics directly. You need to get the parent class to get generics
        clazz = (Class<T>) ((ParameterizedType) this.getClass().getSuperclass().getGenericSuperclass()).getActualTypeArguments()[0]; }}Copy the code

GetActualTypeArguments ()[0] can get the actual generic class object if the current type implements ParameterizedType. Otherwise, getActualTypeArguments() can get the parent of the current type and then get the generic class object.

After the code changes, there are no more typecasting errors, and then we can modify cglib’s proxy object generator and run smoothly.

Successful run!

As you can see, the proxy object generator needs to create a UserDao object through newInstance() to call the methods in BaseDao, so we need to make some adjustments here.

We can’t create a common Dao because our entity type is written directly into the generic, so we can only cache daOs in the system.

When these common methods are encountered, the Dao singleton is invoked directly; the rest of the methods are executed through SQL in the assembled annotations.

Proxy Dao generator

Basically just copy and paste the generator from our previous proxy object.

DaoCglibDynamicProxy:

All that is left is to do Dao singleton caching and public method identification, and ultimately to implement custom SQL.

The first two functions are used to call public methods written on BaseDao, and the last one starts to assemble SQL again.

Dao singleton caching

For now, we’ll just call the methods BaseDao already implements as tests.

Start implementing Dao singleton caching:

We can through DaoCglibDynamicProxy. The first parameter to intercept method to obtain we want the Dao class object.

We can then create a UserDao object from this class object and store it into a map with the fully qualified name of the class as the key and the UserDao object as the value.

Of course, we would have taken the map first rather than creating a new one, and we would have double-checked it for thread safety.

New code in DaoCglibDynamicProxy:

// Object cache map
private static Map<String, Object> objectCacheMap = new HashMap<>();

private Object getDaoFromCache(Class clazz) throws ClassNotFoundException {
    return getDaoFromCache(clazz.getName());
}

private Object getDaoFromCache(String clazzName) {
    Object o = objectCacheMap.get(clazzName);
    if (o==null) {synchronized (objectCacheMap){
            // Lock the objectCacheMap
            o = objectCacheMap.get(clazzName);
            if(o==null) {// If null, create an objectClass<? > clazz =null;
                try {
                    clazz = Class.forName(clazzName);
                    o = clazz.newInstance();
                    objectCacheMap.put(clazzName,o);
                } catch(Exception e) { e.printStackTrace(); }}}}return o;
}
Copy the code

The corresponding intercept() has been modified:

The test.

Once the test passes, we move on to the next feature, public method identification

Common method identification

We can write the names of all public methods in a string and determine if the method currently in intercept() is in it. If so, we can fetch Dao singletons and call public methods. If not, we can customize SQL logic.

Because when we develop BaseDao IBaseDao interface is implemented, and also be IBaseDao interface methods in the public methods, so we can pass IBaseDao. Class. GetMethods () access to all the public methods, We just need to concatenate the names of the Method objects together.

New code in DaoCglibDynamicProxy:

private static String publicMethodsStr = null;

static {
    StringBuilder builder = new StringBuilder();
    for (Method method : IBaseDao.class.getMethods()) {
        builder.append(method.getName()).append(",");
    }
    publicMethodsStr = builder.toString();
}
Copy the code

DaoCglibDynamicProxy. Intercept () is changed to the following:

@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
    if (publicMethodsStr.contains(method.getName())){
        // Is a public method
        Object newInstance = getDaoFromCache(o.getClass().getSuperclass());
        return method.invoke(newInstance, objects);
    }
    // Not a public method

    return null;
}
Copy the code

I tested it and it passed, so let’s move on to developing custom SQL.

GetProxyInstance () can use generics, which saves us casting the type again, so we changed getProxyInstance().

public static <T> T getProxyInstance(Class<T> clazz){ //1. Enhancer en = new Enhancer(); //2. Set the parent class en.setsuperclass (clazz); //3. Set the callback function en.setcallback (proxy); Return (T) en.create(); return (T) en.create(); }Copy the code

Finally, it’s time to develop custom SQL

We have two requirements here:

One is able to precompile, which is to use #{name} as a placeholder, and replace it with? , and then set the corresponding parameters in the precompile SQL;

The other is the ability to replace strings unprecompiled, using ${name} as a placeholder to replace the corresponding arguments before precompiling.

ExecuteInsert executes insert, executeQuery executes select, executeUpdate executes update, delete, Therefore, we need to create at least three annotations to distinguish between different SQL types and let the corresponding TYPE of SQL call the corresponding execution method.

Also, all three methods are private, so we need to create an IExecutor to interface to all three methods, and then have BaseDao implement this interface and open all three methods.

So we need to do two things:

  1. Create an interface that only has execution methods and letBaseDaoImplement this interface.
  2. Create three annotations.
  3. Parse annotations when calling non-public methods.

IExecutor

An executor interface with only three methods is relatively simple.

IExecutor:

/** * executor interface *@param <T>
 */
public interface IExecutor<T> {

    Long executeInsert(String sql, List<String> paramSequence, Map<String, Object> paramMap) ;

    List<T> executeQuery(String sql, List<String> paramSequence, Map<String, Object> paramMap) ;

    int executeUpdate(String sql, List<String> paramSequence, Map<String, Object> paramMap);
}
Copy the code

BaseDao:

As for the modification of the 3 methods, I will not show you. I need to change private to public.

Three annotations

I’m going to call these three annotations InsertSql, SelectSql, UpdateSql, and just have one value, so we don’t have to do that.

Have you forgotten how to write notes? Forget to review 👉 annotations

SelectSql

ExecuteSelect SQL */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface SelectSql {
    String value(a);
}
Copy the code

InsertSql

/** * executeInsert execute SQL */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface InsertSql {
    String value(a);
}
Copy the code

UpdateSql

@target (ElementType.METHOD) @Retention(retentionPolicy.runtime) public @interface UpdateSql { String value(); }Copy the code

Ok, let’s write a method in SelectSql that we can test later.

I’m going to use a map as a parameter, and we can call the execution method directly, and we don’t need to convert, which is the same mistake we made in the previous development, so this is limited.

Analysis of annotation

I’m going to create an SqlInfo to parse annotations on methods and store SQL information in SqlInfo objects.

The intercept method of DaoCglibDynamicProxy is modified, and the execute method is added.

DaoCglibDynamicProxy:

@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
    if (publicMethodsStr.contains(method.getName())){
        // Is a public method
        Object newInstance = getDaoFromCache(o.getClass().getSuperclass());
        return method.invoke(newInstance, objects);
    }
    // Not a public method
    Map<String,Object> params = (Map<String, Object>) objects[0];

    // DaOs inherit from IExecutor
    IExecutor executor = (IExecutor)getDaoFromCache(o.getClass().getSuperclass());

    SqlInfo sqlInfo = SqlInfo.getInstance(o.getClass().getSuperclass(),method);

    return execute(sqlInfo.getSql(params),sqlInfo.getParamSequence(),params,sqlInfo.getSqlClass(),executor);
}

/** * determine which method to execute, determine to execute SQL. *@param executor
 * @return* /
public  Object execute(String sql,List<String> paramSequence,Map<String,Object> params,Class sqlClass,IExecutor executor) {
    if (sqlClass == InsertSql.class) {
        return executor.executeInsert(sql,paramSequence,params);
    } else if (sqlClass == SelectSql.class) {
        return executor.executeQuery(sql,paramSequence,params);
    } else if (sqlClass == UpdateSql.class) {
        return executor.executeUpdate(sql,paramSequence,params);
    }
    return null;
}
Copy the code

A new SqlInfo class was added:

SqlInfo:

/** * Sql information class * stores parsed Sql, and provides non-precompiled processing methods */
public class SqlInfo {

    /** * public List
      
        executeQuery(String SQL, List
       
         paramSequence, Map
        
          paramMap) SQL, paramSequence, and paramMap. * paramMap is provided to us directly by the user, so we need to parse to get SQL, paramSequence */
        ,>
       
      
    // SQL not replaced by ${}
    private String sql = null;

    // A list of arguments to be precompiled
    private List<String> paramSequence = null;

    // Non-precompiled argument list to replace ${}
    private List<String> notPrecompiledParamSequence = null;

    // Annotate the class object
    private Class sqlClass = null;

    / / sqlInfo cache
    private static Map<String, SqlInfo> sqlInfoCacheMap = new HashMap<>();

    // External SqlInfo objects cannot be created, they can only be obtained by getInstance
    private SqlInfo(String sql, List<String> paramSequence, List<String> notPrecompiledParamSequence, Class sqlClass) {
        this.sql = sql;
        this.paramSequence = paramSequence;
        this.notPrecompiledParamSequence = notPrecompiledParamSequence;
        this.sqlClass = sqlClass;
    }

    /** * return an sqlInfo based on class and method, or create a ** if there is none@param clazz
     * @param method
     * @return* /
    public static SqlInfo getInstance(Class clazz, Method method) {
        String sqlStr = null;
        Class sqlClass = null;
        String functionName = clazz.getName() + "." + method.getName();
        Annotation[] annotations = method.getAnnotations();
        for (int i = 0; i < annotations.length; i++) {
            Annotation item = annotations[i];
            // Recognize various annotations
            if (item.annotationType() == InsertSql.class) {
                InsertSql sqlAnnotation = (InsertSql) item;
                sqlStr = sqlAnnotation.value();
                sqlClass = InsertSql.class;
            } else if (item.annotationType() == SelectSql.class) {
                SelectSql sqlAnnotation = (SelectSql) item;
                sqlStr = sqlAnnotation.value();
                sqlClass = SelectSql.class;
            } else if(item.annotationType() == UpdateSql.class) { UpdateSql sqlAnnotation = (UpdateSql) item; sqlStr = sqlAnnotation.value(); sqlClass = UpdateSql.class; }}/ / get sqlInfo
        return getSqlInfo(functionName, sqlStr,sqlClass);
    }

    /** * get an sqlInfo object with the fully qualified class name + method name. * *@paramFunctionName Fully qualified class name + method name *@paramSqlStr SQL * to be executed@paramSqlClass The class object * of the current annotation@return* /
    private static SqlInfo getSqlInfo(String functionName, String sqlStr, Class sqlClass) {
        SqlInfo info = sqlInfoCacheMap.get(functionName);
        // Double check as usual
        if (info == null) {
            synchronized (sqlInfoCacheMap) {
                info = sqlInfoCacheMap.get(functionName);
                if (info == null) { info = creatSqlInfo(sqlStr,sqlClass); sqlInfoCacheMap.put(functionName, info); }}}return info;
    }

    /** * Create an sqlInfo object based on SQL **@param sqlStr
     * @param sqlClass
     * @return* /
    private static SqlInfo creatSqlInfo(String sqlStr, Class sqlClass) {
        StringBuilder builder = new StringBuilder(sqlStr);
        List<String> paramSequence = new ArrayList<>();
        List<String> notPrecompiledParamSequence = new ArrayList<>();
        / / parsing # {}
        // Get the subscript of the #{character and the subscript of the} character, intercept the middle character, and replace
        while (builder.indexOf("# {") > 0) {
            int startIndex = builder.indexOf("# {");
            int endIndex = builder.indexOf("}", startIndex);
            String attribute = builder.substring(startIndex + 2, endIndex);
            paramSequence.add(attribute);
            builder.replace(startIndex, endIndex + 1."?");
        }

        / / parsing ${}
        while (builder.indexOf("${") > 0) {
            int startIndex = builder.indexOf("${");
            int endIndex = builder.indexOf("}", startIndex);
            String attribute = builder.substring(startIndex + 2, endIndex);
            notPrecompiledParamSequence.add(attribute);
            builder.replace(startIndex, endIndex + 1, attribute);
        }

        SqlInfo sqlInfo = new SqlInfo(builder.toString(), paramSequence, notPrecompiledParamSequence,sqlClass);

        return sqlInfo;
    }

    /** * get SQL. * If SQL has ${}, it is necessary to implement non-precompiled string replacement * *@paramParamsMap Parameter used for string replacement *@return* /
    public String getSql(Map paramsMap) {
        if (this.notPrecompiledParamSequence.size() == 0) {
            // Return SQL without string substitution.
            return this.sql;
        }
        // SQL with string substitution
        String sqlStr = this.sql;
        for (String attributeName : this.notPrecompiledParamSequence) {
            String param = String.valueOf(paramsMap.get(attributeName));
            sqlStr = sqlStr.replace(attributeName,param);
        }
        return  sqlStr;
    }

    public List<String> getParamSequence(a){
        return this.paramSequence;
    }

    public Class getSqlClass(a){
        if (this.sqlClass == null) {throw new NullPointerException("this.sqlClass is null.");
        }
        return this.sqlClass; }}Copy the code

The last

In order not to miss the deadline for creative boot camp, I posted it early.

The above code has passed the temporary test, I will test it in detail when I have time, and supplement the test content.

The latest code has been submitted to Gitee at gitee.com/xuxiaojian1… , welcome to download and play.

Although this ORM is relatively simple, it took me a few weeks to complete, so please give it a thumbs up at 👍.