В этой статье я опишу создание фреймворка для автоматической генерации SQL-запросов на основе классов и объектов Java. Я понимаю, что уже существует множество готовых подобных решений, но мне захотелось реализовать это самому.
Для создания фреймворка будем использовать Java-аннотации и Java Reflection API.
Итак, начнем.
Допустим, у нас есть некий класс Person:
Следующий вызов выдаст SQL-запрос для создания таблицы на основе этого класса:
Запустив, получим в консоли следующий вывод:
Теперь пример посложнее, с использованием аннотаций:
На основе данного класса получим следующий SQL-запрос:
Так же я создал класс MySQLClient, который умеет подключаться к серверу базы данных и отправлять туда сгенерированные SQL-запросы.
Клиент содержит следующие методы: createTable, alterTable, insert, update, select.
Используется он примерно так:
Сперва алгоритм с помощью Reflection API перебирает все public и не-static поля класса. Если поле при этом имеет поддерживаемый алгоритмом тип (поддерживаются все примитивные типы данных, их объектные аналоги, а так же тип String), то из объекта Field создается объект Column, содержащий данные о поле таблицы базы данных. Конвертация между типами данных Java и типами MySQL происходит автоматически. Так же из аннотаций поля и класса извлекаются все модификаторы таблицы и ее полей. Затем уже из всех Column формируется SQL-запрос:
Аналогичным образом формируются запросы ALTER TABLE, INSERT и UPDATE. В случае двух последних помимо списка Column из объекта так же извлекаются значения его полей:
Так же в фреймворке есть класс ResultSetExtractor, метод которого extractResultSet(ResultSet resultSet, Class clazz) автоматически создает из resultSet список объектов класса clazz. Делается это довольно просто, так что расписывать принцип его действия я здесь не буду.
На github можно посмотреть полный исходный код фреймворка. На этом у меня все.
Для создания фреймворка будем использовать Java-аннотации и Java Reflection API.
Итак, начнем.
Начнем, пожалуй, с примеров использования
Пример №1
Допустим, у нас есть некий класс Person:
public static class Person { public String firstName; public String lastName; public int age; }
Следующий вызов выдаст SQL-запрос для создания таблицы на основе этого класса:
System.out.println(MySQLQueryGenerator.generateCreateTableQuery(Person.class));
Запустив, получим в консоли следующий вывод:
CREATE TABLE `Person_table` ( `firstName` VARCHAR(256), `lastName` VARCHAR(256), `age` INT);
Пример №2
Теперь пример посложнее, с использованием аннотаций:
@IfNotExists // Добавлять в CREATE-запрос IF NOT EXISTS @TableName("persons") // Произвольное имя таблицы public static class Person { @AutoIncrement // Добавить модификатор AUTO_INCREMENT @PrimaryKey // Создать на основе этого поля PRIMARY KEY public int id; @NotNull // Добавить модификатор NOT NULL public long createTime; @NotNull public String firstName; @NotNull public String lastName; @Default("21") // Значение по умолчанию public Integer age; @Default("") @MaxLength(1024) // Длина VARCHAR public String address; @ColumnName("letter") // Произвольное имя поля public Character someLetter; }
На основе данного класса получим следующий SQL-запрос:
CREATE TABLE IF NOT EXISTS `persons` ( `id` INT AUTO_INCREMENT, `createTime` BIGINT NOT NULL, `firstName` VARCHAR(256) NOT NULL, `lastName` VARCHAR(256) NOT NULL, `age` INT DEFAULT '21', `address` VARCHAR(1024) DEFAULT '', `letter` VARCHAR(1), PRIMARY KEY (`id`));
Пример №3
Так же я создал класс MySQLClient, который умеет подключаться к серверу базы данных и отправлять туда сгенерированные SQL-запросы.
Клиент содержит следующие методы: createTable, alterTable, insert, update, select.
Используется он примерно так:
MySQLClient client = new MySQLClient("login", "password", "dbName"); client.connect(); // Подключаемся к БД client.createTable(PersonV1.class); // Создаем таблицу client.alterTable(PersonV1.class, PersonV2.class); // Изменяем таблицу PersonV2 person = new PersonV2(); person.createTime = new Date().getTime(); person.firstName = "Ivan"; person.lastName = "Ivanov"; client.insert(person); // Добавляем запись в таблицу person.age = 28; person.createTime = new Date().getTime(); person.address = "Zimbabve"; client.insert(person); person.createTime = new Date().getTime(); person.firstName = "John"; person.lastName = "Johnson"; person.someLetter = 'i'; client.insert(person); List selected = client.select(PersonV2.class); // Извлекаем из таблицы все данные System.out.println("Rows: " + selected.size()); for (Object obj: selected) { System.out.println(obj); } client.disconnect(); // Отключаемся от БД
Как это работает
Сперва алгоритм с помощью Reflection API перебирает все public и не-static поля класса. Если поле при этом имеет поддерживаемый алгоритмом тип (поддерживаются все примитивные типы данных, их объектные аналоги, а так же тип String), то из объекта Field создается объект Column, содержащий данные о поле таблицы базы данных. Конвертация между типами данных Java и типами MySQL происходит автоматически. Так же из аннотаций поля и класса извлекаются все модификаторы таблицы и ее полей. Затем уже из всех Column формируется SQL-запрос:
public static String generateCreateTableQuery(Class clazz) throws MoreThanOnePrimaryKeyException { List<Column> columnList = new ArrayList<>(); Field[] fields = clazz.getFields(); // получаем массив полей класса for (Field field: fields) { int modifiers = field.getModifiers(); if (Modifier.isPublic(modifiers) && !Modifier.isStatic(modifiers)) { // если public и не static Column column = Column.fromField(field); // преобразуем Field в Column if (column!=null) columnList.add(column); } } /* из полученных Column генерируем запрос */ } /***************************/ public static Column fromField(Field field) { Class fieldType = field.getType(); // получаем тип поля класса ColumnType columnType; if (fieldType == boolean.class || fieldType == Boolean.class) { columnType = ColumnType.BOOL; } /* перебор остальных типов данных */ { } else if (fieldType==String.class) { columnType = ColumnType.VARCHAR; } else { // Если тип данных не поддерживается фреймворком return null; } Column column = new Column(); column.columnType = columnType; column.name = field.getName(); column.isAutoIncrement = field.isAnnotationPresent(AutoIncrement.class); /* перебор остальных аннотаций */ if (field.isAnnotationPresent(ColumnName.class)) { // если установлено произвольное имя таблицы ColumnName columnName = (ColumnName)field.getAnnotation(ColumnName.class); String name = columnName.value(); if (!name.trim().isEmpty()) column.name = name; } return column; }
Аналогичным образом формируются запросы ALTER TABLE, INSERT и UPDATE. В случае двух последних помимо списка Column из объекта так же извлекаются значения его полей:
Column column = Column.fromField(field); if (column!=null) { if (column.isAutoIncrement) continue; Object value = field.get(obj); if (value==null && column.hasDefaultValue) continue; // есть один нюанс: для корректной работы значений по умолчанию предпочтительно использовать объектные типы вместо примитивных if (column.isNotNull && value==null) { throw new NotNullColumnHasNullValueException(); } String valueString = value!=null ? "'" + value.toString().replace("'","\\'") + "'" : "NULL"; String setValueString = "`"+column.name+"`="+valueString; valueStringList.add(setValueString); }
Так же в фреймворке есть класс ResultSetExtractor, метод которого extractResultSet(ResultSet resultSet, Class clazz) автоматически создает из resultSet список объектов класса clazz. Делается это довольно просто, так что расписывать принцип его действия я здесь не буду.
На github можно посмотреть полный исходный код фреймворка. На этом у меня все.