MySQL
Java
SQL
15 June 2017

Java: автоматически формируем SQL-запросы

В этой статье я опишу создание фреймворка для автоматической генерации SQL-запросов на основе классов и объектов Java. Я понимаю, что уже существует множество готовых подобных решений, но мне захотелось реализовать это самому.

Для создания фреймворка будем использовать 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 можно посмотреть полный исходный код фреймворка. На этом у меня все.

+2
13.5k 50
Comments 19