Яндекс.Метрика

    Песочница

    Микро-ORM своими руками (часть первая)

    Что подвигло меня на написание данной библиотеки и чем плохи существующие решения:
    К сожалению такие монстры как Hibernate «тяжеловесны» и навязывают свой API для работы с БД. Мне же нужна была простенькая библиотечка, использовать которую можно было бы в перемешку с обычным JDBC-кодом (по сути мне нужно было некоторое подобие Dapper.NET для JDBC).

    Основные принципы, используемые при написании библиотеки:
    • простота и атомарность — библиотечка представляет собой 1 java-файл, для добавления в проект достаточно просто добавить файлик к своим исходникам.
    • ненавязчивость — библиотечка не навязывает свой API, возможно использование «вперемешку» с обычным JDBC-кодом
    • независимость — библиотечка не использует ничего кроме Java SE 5
    • расширяемость — библиотечка поддерживает добавление расширений, необходимых для конкретного проекта

    Pеализация метода создания PreparedStatement-а с заполненной коллекцией параметров.

    Хочется писать код такого вида:
    PreparedStatement stmt = DB.prepareStatement(conn, "select id, login, email from users where (login={0} or email={0}) and pass={1}", "dimzon", "pass")

    Здесь значения параметров передаются позиционно а в теле запроса на них ссылаются с помощью конструкции {ИНДЕКС_ПАРАМЕТРА}. Подобная запись достаточна лаконичка и удобна (и привычна программистам на .NET).
    Таким образом имеем следующее объявление метода:
    public static PreparedStatement prepareStatement(Connection cn, CharSequence query, Object... args) throws SQLException

    Реализация состоит из решения 2-х задач:
    1. Парсинг тела запроса на предмет вхождения конструкции {ИНДЕКС_ПАРАМЕТРА}, замена на "?" и составление карты соответствия «индекс в args -> массив индексов в preparedStatement»
    2. Подстановка параметров в PreparedStatement при помощи вызова соответствующих методов setXXXX
    Первая задачка решается с помощью регулярных выражений. А при решении второй задачки возникают ньюансы:
    1. Определение типа SQL-параметра. Для простых случаев (Integer,Date) все просто, но непонятно что делать если в качестве аргумента был передан какой-то нестандартный класс.
    2. Интерпретация null-значений. В Java null нетипизирован (т.е. нельзя сделать null.getClass()) — непонятно какой java.sql.Types передавать в PreparedStatement.setNull.
    В решении обоих проблем нам поможет паттерн «стратегия»:
    
    public interface Param {
    	void set(PreparedStatement ps, int index) throws SQLException;
    }
    

    Жить стало проще — в случае если на входе у нас реализация интерфейса Param то мы просто вызываем метод set. Стандартные случаи (Integer,Date) мы для единообразия тоже заведём через стратегии. Значит нам необходим метод создания правильной стратегии в зависимости от типа параметра (если он изначально не стратегия).
    Тут нам поможет паттерн «фабрика». Но я не стал заводить для фабрики отдельный интерфейс, я написал следующий абстрактный класс (.NET программисты увидят аналог с Func<T,R>):
    
    public static abstract class FN<I,O> {
    	public abstract O apply(I input) throws Exception;
    }
    

    Таким образом фабрика это потомок класса FN<Object,Param>.
    Для быстрого получения экземпляра фабрики по типу используем ConcurrentHashMap<Class,FN<Object,Param>> paramFactories.
    Если добавить в этот Map свою фабрику то мы можем передавать в prepareStatement свой класс as is — появляется ещё 1 метод:
    public static <E> void registerParamFactory(Class<E> forClass, FN<E, Param> factory)

    Соответственно для нестандартной ситуации (расширяемость) можно применить один из двух подходов:
    • явно передать в prepareStatement свою реализацию Param
    • зарегистрировать с помощью registerParamFactory фабрику создания Param

    Последний вопрос — что делать если на вход prepareStatement пришел null. Тип определить невозможно, но если вместо "?" в тело запроса вставить напрямую «NULL» то сервер сам разберется.

    Полный код данного примера:
    
    package a.poor.mans.orm.part1;
    
    import javax.xml.bind.JAXB;
    import java.io.IOException;
    import java.io.StringWriter;
    import java.sql.*;
    import java.util.ArrayList;
    import java.util.Collection;
    import java.util.List;
    import java.util.Map;
    import java.util.concurrent.ConcurrentHashMap;
    import java.util.regex.Matcher;
    import java.util.regex.Pattern;
    
    public class DB {
    	public interface Param {
    		void set(PreparedStatement preparedStatement, int index) throws SQLException;
    	}
    
    	public static abstract class FN<I, O> {
    		public abstract O apply(I input) throws Exception;
    
    		public static <I, O> FN<I, O> constant(final O constValue) {
    			return new FN<I, O>() {
    				public O apply(I input) throws Exception {
    					return constValue;
    				}
    			};
    		}
    	}
    
    	public static Param paramDate(final Date value) {
    		return new Param() {
    			public void set(PreparedStatement preparedStatement, int index) throws SQLException {
    				if (value != null) preparedStatement.setDate(index, value);
    				else preparedStatement.setNull(index, Types.DATE);
    			}
    		};
    	}
    
    	public static Param paramTime(final Time value) {
    		return new Param() {
    			public void set(PreparedStatement preparedStatement, int index) throws SQLException {
    				if (value != null) preparedStatement.setTime(index, value);
    				else preparedStatement.setNull(index, Types.TIME);
    
    			}
    		};
    	}
    
    	public static Param paramTimestamp(final Timestamp value) {
    		return new Param() {
    			public void set(PreparedStatement preparedStatement, int index) throws SQLException {
    				if (value != null) preparedStatement.setTimestamp(index, value);
    				else preparedStatement.setNull(index, Types.TIMESTAMP);
    			}
    		};
    	}
    
    	public static Param paramString(final String value) {
    		return new Param() {
    			public void set(PreparedStatement preparedStatement, int index) throws SQLException {
    				preparedStatement.setString(index, value);
    			}
    		};
    	}
    
    	public static Param paramInteger(final Integer value) {
    		return new Param() {
    			public void set(PreparedStatement preparedStatement, int index) throws SQLException {
    				if (value != null) preparedStatement.setInt(index, value);
    				else preparedStatement.setNull(index, Types.INTEGER);
    			}
    		};
    	}
    
    	public static Param paramLong(final Long value) {
    		return new Param() {
    			public void set(PreparedStatement preparedStatement, int index) throws SQLException {
    				if (value != null) preparedStatement.setLong(index, value);
    				else preparedStatement.setNull(index, Types.BIGINT);
    			}
    		};
    	}
    
    	public static Param paramShort(final Short value) {
    		return new Param() {
    			public void set(PreparedStatement preparedStatement, int index) throws SQLException {
    				if (value != null) preparedStatement.setShort(index, value);
    				else preparedStatement.setNull(index, Types.SMALLINT);
    			}
    		};
    	}
    
    	public static Param paramByte(final Byte value) {
    		return new Param() {
    			public void set(PreparedStatement preparedStatement, int index) throws SQLException {
    				if (value != null) preparedStatement.setByte(index, value);
    				else preparedStatement.setNull(index, Types.TINYINT);
    			}
    		};
    	}
    
    	public static Param paramXML(final String value) {
    		return new Param() {
    			SQLXML sqlxml = null;
    
    			public void set(PreparedStatement preparedStatement, int index) throws SQLException {
    				if (value != null) {
    					if (sqlxml == null) {
    						sqlxml = preparedStatement.getConnection().createSQLXML();
    						sqlxml.setString(value);
    					}
    					preparedStatement.setSQLXML(index, sqlxml);
    				} else {
    					preparedStatement.setNull(index, Types.SQLXML);
    				}
    			}
    		};
    	}
    
    	public static Param paramXML(final SQLXML value) {
    		return new Param() {
    			public void set(PreparedStatement preparedStatement, int index) throws SQLException {
    				if (value == null) preparedStatement.setNull(index, Types.SQLXML);
    				else preparedStatement.setSQLXML(index, value);
    			}
    		};
    	}
    
    
    	private final static Pattern INDEX_PATTERN = Pattern.compile("((?<!\\{)\\{)(\\d+)(\\}(?!\\}))");
    	private final static Pattern ESCAPE_OPEN = Pattern.compile("\\{\\{");
    	private final static Pattern ESCAPE_CLOSE = Pattern.compile("}}", Pattern.LITERAL);
    
    
    	private static String object2xml(Object obj) {
    		StringWriter stringWriter = new StringWriter();
    		try {
    			try {
    				JAXB.marshal(obj, stringWriter);
    			} finally {
    				stringWriter.close();
    			}
    			return stringWriter.toString();
    		} catch (IOException e) {
    			throw new RuntimeException(e.toString(), e);
    		}
    	}
    
    	private final static Map<Class, FN> paramFactories = new ConcurrentHashMap<Class, FN>() {
    		{
    			put(SQLXML.class, new FN<SQLXML, Param>() {
    				public Param apply(SQLXML input) throws Exception {
    					return paramXML(input);
    				}
    			});
    			put(Collection.class, new FN<Collection, Param>() {
    				public Param apply(Collection input) throws Exception {
    					return paramXML(object2xml(input.toArray()));
    				}
    			});
    			put(String.class, new FN<String, Param>() {
    				public Param apply(String input) throws Exception {
    					return paramString(input);
    				}
    			});
    			put(java.sql.Date.class, new FN<java.sql.Date, Param>() {
    				public Param apply(java.sql.Date input) throws Exception {
    					return paramDate(input);
    				}
    			});
    			put(Time.class, new FN<Time, Param>() {
    				public Param apply(Time input) throws Exception {
    					return paramTime(input);
    				}
    			});
    			put(Timestamp.class, new FN<Timestamp, Param>() {
    				public Param apply(Timestamp input) throws Exception {
    					return paramTimestamp(input);
    				}
    			});
    			put(Boolean.class, emptyFN);
    			put(Long.class, emptyFN);
    			put(Integer.class, emptyFN);
    			put(Short.class, emptyFN);
    			put(byte.class, emptyFN);
    		}
    	};
    
    	public static <E> void registerParamFactory(Class<E> forClass, FN<E, Param> factory) {
    		paramFactories.put(forClass, factory);
    		for (Map.Entry<Class, FN> pair : paramFactories.entrySet()) {
    			if (pair.getValue() == emptyFN) pair.setValue(null);
    		}
    	}
    
    	private static FN getParamFactory(Class paramClass) {
    		FN v = getParamFactory(paramClass, paramClass);
    		if (v != null) return v;
    		paramFactories.put(paramClass, emptyFN);
    		return emptyFN;
    	}
    
    	private static FN getParamFactory(Class paramClass, Class targetClass) {
    		FN val;
    		for (Class current = paramClass; current != Object.class && current != null; current = current.getSuperclass()) {
    			val = paramFactories.get(current);
    			if (val != null) {
    				if (current != targetClass) paramFactories.put(targetClass, val);
    				return val;
    			}
    			for (Class intf : current.getInterfaces()) {
    				val = getParamFactory(intf, targetClass);
    				if (val != null) return val;
    			}
    		}
    		return null;
    	}
    
    
    	private static final FN emptyFN = new FN() {
    		public Object apply(Object input) throws Exception {
    			return null;
    		}
    	};
    
    	@SuppressWarnings("unchecked")
    	public static PreparedStatement prepareStatement(final Connection connection, final CharSequence query, final Object... args) throws SQLException {
    		int argc = args.length;
    		String[] strings = new String[argc];
    		Param[] params = new Param[argc];
    		Object[] indexes = new Object[argc];
    
    		for (int i = 0; i < argc; ++i) {
    			Object arg = args[i];
    			if (null == arg) {
    				strings[i] = "NULL";
    			} else if (arg instanceof Param) {
    				params[i] = (Param) arg;
    			} else {
    				Class paramClass = arg.getClass();
    				FN factory = getParamFactory(paramClass);
    				if (factory != null && factory != emptyFN) {
    					try {
    						params[i] = (Param) factory.apply(arg);
    					} catch (Exception e) {
    						throw new RuntimeException(e);
    					}
    				} else {
    					if (arg instanceof Boolean) {
    						strings[i] = ((Boolean) arg) ? "1" : "0";
    					} else if (arg instanceof Integer) {
    						strings[i] = Integer.toString((Integer) arg);
    					} else if (arg instanceof Long) {
    						strings[i] = Long.toString((Long) arg);
    					} else if (arg instanceof Short) {
    						strings[i] = Short.toString((Short) arg);
    					} else if (arg instanceof Byte) {
    						strings[i] = Byte.toString((Byte) arg);
    					} else {
    						params[i] = paramXML(object2xml(arg));
    					}
    				}
    			}
    		}
    
    		Matcher matcher = INDEX_PATTERN.matcher(query);
    		StringBuffer stringBuffer = new StringBuffer();
    		int index = 0;
    		while (matcher.find()) {
    			++index;
    			int reference = Integer.parseInt(matcher.group(2));
    			if (strings[reference] != null) {
    				matcher.appendReplacement(stringBuffer, strings[reference]);
    			} else {
    				List<Integer> lst = (List<Integer>) (null == indexes[reference] ? (indexes[reference] = new ArrayList<Integer>()) : indexes[reference]);
    				lst.add(index);
    				matcher.appendReplacement(stringBuffer, "?");
    			}
    		}
    		matcher.appendTail(stringBuffer);
    		String sql = ESCAPE_CLOSE.matcher(ESCAPE_OPEN.matcher(stringBuffer).replaceAll("{")).replaceAll("}");
    
    		PreparedStatement preparedStatement = connection.prepareStatement(sql);
    		for (int i = 0; i < argc; ++i) {
    			List<Integer> lst = (List<Integer>) indexes[i];
    			if (lst == null) continue;
    			Param param = params[i];
    			for (Integer pos : lst)
    				param.set(preparedStatement, pos);
    		}
    
    		return preparedStatement;
    	}
    
    	public static ResultSet executeQuery(final Connection connection, final CharSequence query, final Object... args) throws SQLException {
    		PreparedStatement preparedStatement = prepareStatement(connection, query, args);
    		try {
    			return preparedStatement.executeQuery();
    		} finally {
    			preparedStatement.close();
    		}
    	}
    
    	public static boolean execute(final Connection connection, final CharSequence query, final Object... args) throws SQLException {
    		PreparedStatement preparedStatement = prepareStatement(connection, query, args);
    		try {
    			return preparedStatement.execute();
    		} finally {
    			preparedStatement.close();
    		}
    	}
    
    	public static int executeUpdate(final Connection connection, final CharSequence query, final Object... args) throws SQLException {
    		PreparedStatement preparedStatement = prepareStatement(connection, query, args);
    		try {
    			return preparedStatement.executeUpdate();
    		} finally {
    			preparedStatement.close();
    		}
    	}
    }