Лабораторна робота 5

Рефлексія. Створення застосунків баз даних

1 Завдання на лабораторну роботу

1.1 Створення консольного застосунку баз даних

Створити консольний застосунок баз даних для роботи з сутностями індивідуальних завдань попередніх лабораторних робіт.

Дані про сутності предметної області повинні зберігатися у реляційній базі даних. Слід застосувати СУБД MySQL (або іншу СУБД, за погодженням з викладачем). У консольному застосунку слід створити дві таблиці реляційної бази даних, відповідно для першої та другої сутності індивідуального завдання (перша та друга таблиця).

Після створення таблиць необхідно виконати такі дії:

  • імпорт в базу даних з JSON-файлу;
  • відображення даних обох таблиць;
  • пошук за ознаками, визначеними в третій лабораторній роботі курсу "Основи програмування Java";
  • здійснення сортування за ознаками, визначеними в четвертій лабораторній роботі курсу "Основи програмування Java";
  • додавання нового запису в таблицю;
  • видалення певних записів з таблиці;
  • експорт з бази даних в JSON-файл.

1.2 Перегляд всіх полів класу

Створити консольний застосунок, в якому користувач вводить ім'я класу й отримує інформацію про всі поля цього класу (включаючи закриті й захищені).

1.3 Створення GUI-застосунку баз даних (додаткове завдання)

Реалізувати мовою Java за допомогою засобів JavaFX застосунок графічного інтерфейсу користувача, в якому здійснюється обробка даних індивідуальних завдань попередніх лабораторних робіт. Дані про сутності предметної області зберігаються у реляційній базі даних. Головне вікно повинно містити меню, в якому необхідно реалізувати функції, перелічені в завданні 1.1. Додавання нового запису в таблицю слід реалізувати в окремому вікні з контролем уведених даних.

У лівій частині вікна слід розташувати область відображення для результатів пошуку, а також кнопки, які забезпечують виконання основних функцій програми. В середній частині вікна слід розташувати таблиці для відображення даних.

1.4 Отримання даних про анотації методу (додаткове завдання)

Створити консольну програму, в якій користувач вводить ім'я методу класу й отримує інформацію про всі анотації з політикою утримання RUNTIME, якими позначений цей метод.

2 Методичні вказівки

2.1 Використання RTTI

Механізм ідентифікації типу часу виконання (RTTI – run-time type identification) дозволяє визначити тип об'єкта під час виконання програми. Механізм RTTI підтримує більшість мов об'єктно-орієнтованого програмування. Цей механізм, як зазвичай, застосовується до так званих поліморфних типів (класів або структур, які надають віртуальні функції). RTTI дозволяє під час виконання програми визначити точний тип об'єкта за посиланням (або вказівником для мов, що підтримує вказівники). Завдяки розширенню правил сумісності типів посилання на базовий тип може фактично вказувати на об'єкти похідних типів, що є основою поліморфізму. У деяких випадках необхідно явно отримати реальний тип об'єкта, або перевірити, чи ми маємо справу з об'єктом типу, що нас цікавить.

У Java всі типи-посилання є поліморфними. RTTI реалізований через інформацію про тип, який зберігається в кожному об'єкті. Ключове слово instanceof дозволяє перевірити, чи є об'єкт екземпляром конкретного типу. Вираз

об'єкт instanceof клас

повертає значення типу boolean, яке може бути використане перед викликом методів, характерних для конкретного класу:

if(x instanceof SomeClass)
    ((SomeClass)x).someMethod();

Без такої перевірки приведення може завершиться генерацією винятку ClassCastException.

У наведеному нижче прикладі залежно від числа, введеного користувачем, створюються об'єкти різних типів:

package ua.inf.iwanoff.java.advanced.fifth;

import java.util.Scanner;

class First {

    void first() {
        System.out.println("First");
    }

}

class Second {

    void second() {
        System.out.println("Second");
    }

}

public class InstanceOf {

    public static void main(String[] args) {
        try {
            Object o;
            int n = new Scanner(System.in).nextInt();
            switch (n) {
                case 1: 
                    o = new First(); 
                    break;
                case 2:
                    o = new Second();
                    break;
                default:
                    return;
            }
            if (o instanceof First) {
                ((First) o).first();
            }
            if (o instanceof Second) {
                ((Second) o).second();
            }
        }
        catch (Exception e) {
            e.printStackTrace();
        }
    }

}

2.2 Використання завантажувачів класів

Однією з основних особливостей платформи Java є модель динамічного завантаження класів. Завантажувачі класів – це спеціальні об'єкти. які забезпечують завантаження коду класів під час роботи програми.

Динамічне завантаження класів в Java має ряд особливостей:

  • відкладене (lazy) завантаження – класи завантажуються тільки в разі потреби, що дозволяє заощаджувати ресурси й поліпшити розподіл навантаження;
  • контроль коректності коду (використання типів) проводиться під час завантаження класу, а не під час виконання коду;
  • можливість створення користувацьких завантажувачів, які повністю контролюють процес отримання запитаного класу;
  • можливість контролювати надійність і безпеку коду через так звані атрибути безпеки.

З кожним завантажувачем пов'язаний свій простір імен для створюваних класів. Класи, створені двома різними завантажувачами на основі загального байт-коду, в системі будуть відрізнятися.

Під час завантаження класу створюється об'єкт типу java.lang.Class. Тип Class – це клас, екземпляри якого представляють класи, інтерфейси та інші типи під час виконання Java-програми. Примітивні типи Java (boolean, byte, char, short, int, long, float і double) і void також представлені у вигляді об'єктів типу Class. Об'єкт типу Class може бути отриманий для типів, що існують (в тому числі й примітивних), з використанням ключового слова class:

Class<?> c1 = String.class;
Class<?> c2 = double.class;

Як видно з попереднього прикладу, клас Class – узагальнений. Найбільш часто його використовують з параметром <?> (значення типу визначається автоматично).

Можна також отримати об'єкт Class для раніше створеного об'єкта типу посилання:

Integer i = new Integer(100);
Class<?> c3 = i.getClass();

Об'єкт типу Class дозволяє отримати різноманітну інформацію про тип, яка може бути використана під час виконання програми і є основою рефлексії. Ці можливості будуть розглянуті нижче.

Далі термін "завантаження класів" передбачатиме завантаження різних типів.

Розрізняють три види завантажувачів у Java:

  • базовий завантажувач (bootstrap) – завантажує базові (системні) класи, які зазвичай знаходяться в jar-файлах в директорії jre/lib; базовий завантажувач не доступний програмно;
  • завантажувач розширень (Extension Classloader) – завантажує класи різних пакетів розширень, які зазвичай містяться в jre/lib/ext;
  • системний завантажувач (System Classloader) – реалізує стандартний алгоритм завантаження з каталогів і JAR-файлів, перелічених у змінній CLASSPATH.

Абстрактний клас java.lang.ClassLoader використовується для створення типів об'єктів, що відповідають за завантаження класів. Кожен об'єкт типу Class містить посилання на ClassLoader. Для кожного класу визначений поточний завантажувач. Для того, щоб отримати завантажувач, яким був завантажений клас SomeClass, необхідно скористатися методом SomeClass.class.getClassLoader(). Java дозволяє створювати користувацькі завантажувачі.

Під час завантаження класів використовують спеціальну ієрархію об'єктів-завантажувачів (базовий завантажувач / завантажувач розширень / системний завантажувач). Алгоритм завантаження зазвичай такий:

  • здійснюється пошук в списку раніше завантажених класів (щоб клас не був завантажений двічі);
  • якщо клас не був завантажений, здійснюється делегування батьківському завантажувачу;
  • якщо батьківський завантажувач не зміг завантажити запитаний клас, поточний завантажувач намагається сам завантажити необхідний клас.

При цьому слід врахувати, що всі "системні" класи (java.lang.Object, java.lang.String тощо) завантажуються тільки базовим завантажувачем. Для них getClassLoader() повертає null.

2.3 Рефлексія

Розглянутий раніше механізм RTTI базується на інформації, яка може бути отримана під час компіляції. Поряд з RTTI, Java надає більш складний і потужний механізм – рефлексію. Цей механізм використовує інформацію, яка може бути недоступна під час компіляції.

Відображення або рефлексія (reflection) – це механізм, який дозволяє програмі відстежувати й модифікувати власну структуру та поведінку під час виконання.

Безпосередня інформація про тип зберігається в об'єкті типу java.lang.Class. Пакет java.lang.reflect містить класи, які надають інформацію про класи та об'єкти та забезпечують доступ до елементів класів та інтерфейсів.

За допомогою можливостей типу Class можна створити об'єкт класу з певним ім'ям. Для одержання об'єкта типу Class використовується статичний метод forName().

String name = "SomeClass";
Class c = Class.forName(name);

У наведеному нижче прикладі ім'я необхідного класу вводиться з клавіатури, після чого створюється об'єкт за допомогою функції newInstance():

package ua.inf.iwanoff.java.advanced.fifth;

import java.util.Scanner;

class One {

    void one() {
        System.out.println("one");
    }

}

public class NewInstance {

    public static void main(String[] args) {
        try {
            String name = new Scanner(System.in).next();
            Object o = Class.forName(name).newInstance();
            if (o instanceof One) {
                ((One) o).one();
            }
        }
        catch (Exception e) {
            e.printStackTrace();
        }
    }

}

Об'єкт типу Class містить, зокрема, інформацію про ім'я типу, що у наведеному вище прикладі можна було б одержати в такий спосіб:

System.out.println(o.getClass().getName());

Інформація про типи може бути представлена в зручному вигляді за допомогою функцій getSimpleName() або getCanonicalName().

Об'єкт типу Class може бути також отриманий для інтерфейсів і навіть для безіменних класів. Метод isInterface() для інтерфейсів повертає true, а метод isAnonymousClass() повертає true для безіменних класів. Можна також отримати масив вкладених типів, клас, в який вкладено даний тощо.

Метод getSuperclass() повертає базовий клас. Для класу Object цей метод повертає null. Метод getInterfaces() повертає масив реалізованих інтерфейсів (також об'єктів типу Class).

Можна отримати список методів класу – масив об'єктів класу java.lang.reflect.Method. Цей масив буде містити посилання на відкриті методи даного класу і всіх його предків:

for (int i = 0; i < o.getClass().getMethods().length; i++) { 
    System.out.println(o.getClass().getMethods()[i].getName());
}

Функція getMethod() дозволяє отримати метод за ім'ям.

Функція getDeclaredMethods() дозволяє отримати список всіх методів (відкритих, захищених, пакетних і закритих), але тільки визначених у цьому класі (а не в базових класах). Аналогічно викликають функцію getDeclaredMethod(), яка дозволяє отримати будь-який метод за ім'ям.

Для методу можна отримати інформацію про клас, в якому він оголошений (getDeclaringClass()), про ім'я (getName()), тип результату (getReturnType()). Можна також отримати масив типів параметрів (об'єктів типу Class<?>) за допомогою методу getParameterTypes(). Масив, що отримують за допомогою функції getExceptionTypes(), містить типи можливих винятків. Перевизначена функція toString() повертає повну сигнатуру методу зі списком винятків.

Інформація про модифікатори методу може бути отримана за допомогою функції getModifiers(). Ця функція повертає ціле число, яке може бути використано, наприклад, для функції charAt() класу String:

  Class<?> c = String.class;
  String methodName = "charAt";
  Method m = c.getDeclaredMethod(methodName, int.class); // Визначаємо метод з цілим параметром
  int mod = m.getDeclaringClass().getModifiers();
  if (Modifier.isAbstract(mod)) {
      System.out.print("abstract ");
  }
  if (Modifier.isPublic(mod)) {
      System.out.print("public ");
  }
  if (Modifier.isPrivate(mod)) {
      System.out.print("private ");
  }
  if (Modifier.isProtected(mod)) {
      System.out.print("protected ");
  }
  if (Modifier.isFinal(mod)) {
      System.out.print("final ");
  }

Метод можна викликати за допомогою функції invoke(). Перший параметр цього методу – посилання на об'єкт класу, для якого викликають метод. Для статичних методів – це null. Далі вказують необхідні фактичні параметри. Оскільки кілька методів може відрізнятися тільки списком параметрів, під час виклику getMethod()крім імені обов'язково потрібно вказувати типи параметрів (масив об'єктів типу Class). Якщо список параметрів порожній, цей масив можна опустити.

Наприклад, так можна викликати статичну функцію без параметрів для класу, заданого ім'ям:

method = Class.forName(className).getMethod(methodName);
method.invoke(null);

У наведеному нижче прикладі здійснюється виклик нестатичного методу:

package ua.inf.iwanoff.java.advanced.fifth;

import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.List;

public class ListSize {

    public static void main(String[] args) throws Exception {
        List<String> list = Arrays.asList("one", "two");
        Method method = Class.forName("java.util.List").getMethod("size");
        System.out.println(method.invoke(list));
    }

}

Отримання інформації про поля здійснюється аналогічно. Клас java.lang.reflect.Field представляє дані про поле класу – імені (getName()), типу (getType()), клас, в якому визначено поле (getDeclaringClass()), модифікатори доступу (getModifiers()) тощо. Для отримання значення, яке зберігається в поле, використовують метод get(), який повертає результат типу Object. За допомогою методу set() можна встановити нове значення.

Якщо ми хочемо отримати доступ до закритих або захищених полів, але визначених саме в цьому класі, використовують методи getDeclaredField() (за ім'ям) і getDeclaredFields() (для отримання масиву полів).

Через механізм рефлексії можна обійти обмеження доступу до елементів класу. Закритий метод, отриманий за допомогою getDeclaredMethod() або як елемент масиву getDeclaredMethods(), не може бути безпосередньо викликаний. Однак для його виклику можна дозволити доступ за допомогою функції setAccessible() з параметром true. Так само можна дістатись закритих полів. Наведений нижче приклад демонструє можливості роботи з закритими елементами класу:

package ua.inf.iwanoff.java.advanced.fifth;

import java.lang.reflect.Field;
import java.lang.reflect.Method;

class TestClass {
    private int k = 0;

    private void testPrivate() {
        System.out.println("Закритий метод. k = " + k);
    }

}

public class AccessTest {

    public static void main(String[] args) throws Exception {
        // Отримуємо клас:
        Class<?> c = Class.forName("ua.inf.iwanoff.java.advanced.fifth.TestClass");
        // Створюємо об'єкт:
        Object obj = c.newInstance();
        // Доступ до закритого поля:
        Field f = c.getDeclaredField("k");
        f.setAccessible(true);
        f.set(obj, 1);
        // Доступ до закритого методу:
        Method m = c.getDeclaredMethod("testPrivate");
        m.setAccessible(true);
        m.invoke(obj);
    }

}

Механізм рефлексії слід застосовувати з обережністю, оскільки він надає можливості обходу інкапсуляції.

2.4 Створення та використання анотацій

2.4.1 Поняття анотації. Стандартні анотації

Починаючи з версії JDK 1.5 Java підтримує механізм анотацій. Анотація є спеціальним тегом, що додається до будь-якого оголошення. У тексті програми анотації починаються із символу @. Анотації часто відносять до так званих метаданих (даних про дані), однак у строгому значенні слова це дані про сирцевий код. Додавання анотацій до коду саме собою не впливає виконання програми. Анотації використовуються зовнішніми інструментами – компілятором, генераторами класів, візуальними редакторами тощо. Іншими словами, потрібен спеціальний оброблювач сирцевого коду, який використовує інформацію, що надається анотаціями.

Анотації можуть бути без параметрів (властивостей), наприклад @Override, такі що вимагають вказівки будь-якої властивості, @SuppressWarnings("resource"), або вимагають вказівки кількох властивостей у вигляді пар ключ/значення, наприклад @GenericGenerator (name="increment", strategy = "increment") (останній приклад належить до бібліотеки Hibernate).

Анотації можуть відрізнятися своїм життєвим циклом, що визначається так званою "політикою утримання" (annotation retention policies). Відповідно до свого життєвого циклу анотації можуть бути поділені на три групи, що визначаються переліком java.lang.annotation.RetentionPolicy:

  • SOURCE: анотації зберігаються лише у вихідному файлі та відкидається під час компіляції
  • CLASS: анотації зберігаються у файлі .class під час компіляції, проте недоступні під час виконання
  • RUNTIME: анотації зберігаються у файлі .class та залишаються доступними JVM під час виконання.

Раніше згадувалося використання анотацій @Override, @SuppressWarnings та @Deprecated. Це анотації використовуються компілятором. До них можна також додати інструкцію @SafeVarargs, що з'явилася в Java 7. Остання використовується над узагальненими функціями зі змінним числом параметрів і може бути використана замість @SuppressWarnings("unchecked").

Наприклад, під час компіляції такої функції

static <T>void f(T... a) {
    for (T t : a) {
        System.out.println(t);
    }
}

компілятор видасть попередження, пов'язане з потенційним засміченням динамічної пам'яті: "Type safety: Potential heap pollution via varargs parameter a". Найпростіший спосіб придушити це повідомлення – використання @SafeVarargs перед методом.

2.4.2 Користувацькі анотації

Для створення власної анотації використовується ключове слово @interface, після якого визначається ім'я майбутньої анотації. В тілі анотації можна розташовувати оголошення функцій, що повертають значення примітивних типів або String. Наприклад:

public @interface NewAnnotation {
    int firstValue();
    String secondValue() default "second";
}

Перед описом анотації доцільно вказати цільовий об'єкт та "політику утримання", наприклад:

package ua.inf.iwanoff.java.advanced.fifth;

import java.lang.annotation.*;

@Target(value=ElementType.TYPE)
@Retention(value=RetentionPolicy.RUNTIME)
public @interface NewAnnotation {
    int firstValue();
    String secondValue() default "second";
}

Як видно з прикладу, відповідні анотації вимагають підключення пакета java.lang.annotation.

Цільовий об'єкт – це елемент сирцевого коду, до якого буде відноситись інструкція. Перелік java.lang.annotation.ElementType крім значення TYPE (анотація розташовується перед описом типів), надає значення FIELD (перед визначенням поля), METHOD (перед визначенням методу), PARAMETER (перед описом параметра функції), ANNOTATION_TYPE (перед описом іншої анотації) а також CONSTRUCTOR , LOCAL_VARIABLE і PACKAGE.

Методи в тілі опису анотації визначають властивості – ключі, значення яких зазначаються під час використання анотації. Методи всередині анотації не можуть мати параметрів. Їхні імена завжди починаються з маленької літери Типи результату функцій – стандартні примітивні типи та String.

Модифікатор default вказує усталене значення, що робить явне визначення відповідного значення необов'язковим. Відсутність модифікатора default робить властивість обов'язковою – його значення обов'язково потрібно вказувати у дужках під час використання анотації.

Можна навести приклад використання описаної анотації:

package ua.inf.iwanoff.java.advanced.fifth;

@NewAnnotation(firstValue=10, secondValue="A")
public class SomeAnnotatedClass {

}

Залишилося з'ясувати, як цю інформацію можна використовувати під час виконання програми. Для цього використовуються механізми рефлексії. Необхідно створити зовнішній оброблювач анотацій. Спочатку завантажуємо клас, наприклад за ім'ям. Далі отримуємо його анотації та перевіряємо наявність нашої анотації. Якщо анотація є, виводимо значення її властивостей:

package ua.inf.iwanoff.java.advanced.fifth;

public class NewAnnotationProcessor {

    public static void main(String[] args) {
        try {
            Class<?> cls = Class.forName("ua.inf.iwanoff.java.advanced.fifth.SomeAnnotatedClass");
            if (cls.isAnnotationPresent(NewAnnotation.class)) {
                NewAnnotation ann = cls.getAnnotation(NewAnnotation.class);
                System.out.println(ann.firstValue());
                System.out.println(ann.secondValue());
            }
            else {
                System.out.println("No such annotation!");
            }
        }
        catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }

}

Крім зазначених методів isAnnotationPresent() і getAnnotation() тип Class<> підтримує також методи getAnnotations() (повертає масив посилань на java.lang.annotation.Annotation) та isAnnotation() (перевіряє, чи сам тип анотацією).

Клас java.lang.annotation.Annotation надає функцію annotationType() (повертає Class<> – посилання на тип анотації).

Для створення нової анотації в середовищі IntelliJ IDEA можна скористатися функцією меню File | New | Java Class і вибрати Annotation зі списку. У створеному коді додаємо необхідні анотації перед нашою новою анотацією, використовуючи технологію IntelliSense (набираємо @, потім Ctrl-пропуск і вибираємо необхідну анотацію).

2.5 Використання анотацій в бібліотеках і фреймворках Java

2.5.1 Загальні концепції

Анотації надають потужний механізм розширення можливостей синтаксису Java. Практично анотації використовують скрізь, де передбачається будь-яке маніпулювання сирцевим кодом.

Численні каркаси програм побудовані на використанні анотацій. Зокрема, анотації інтенсивно використовуються у Spring Framework. Специфікація Enterprise JavaBeans 3.0 використовує інструкції для забезпечення свободи іменування методів. Специфікація Java Persistence API використовує інструкції для забезпечення об'єктно-реляційного відображення. Анотації використовуються також засобами тестування JUnit.

Для роботи з деякими з цих бібліотек у середовищі IntelliJ IDEA необхідно дозволити обробку анотацій (File | Settings, далі Build, Execution, Deployment | Compiler | Annotation Processors, вмикаємо Enable annotation processing)

2.5.2 Використання бібліотеки Lombok

Як приклад можна навести бібліотеку Project Lombok, яка може бути використана для зменшення однотипного коду (boilerplate code). Lombok може автоматично генерувати гетери та сетери, конструктори, генерацію хеш-кодів, перевірку еквівалентності, перетворення на рядок та багато іншого за допомогою анотацій. Бібліотека отримала назву на честь індонезійського острова Ломбок. У перекладі з індонезійської Lombok означає "перець чилі".

Для роботи застосунку з бібліотекою Project Lombok у файлі pom.xml слід додати таку залежність:

<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.26</version>
</dependency>

Версія бібліотеки може змінюватися.

Потім слід перезавантажити проєкт Maven (знаходячись у вікні з файлом pom.xml, в контекстному меню треба вибрати Maven | Reload project).

Сирцевий код, який використовує Lombok, не є валідним кодом Java. Тому для роботи з Project Lombok в IDE треба встановлювати необхідний плагін. Починаючи з IntelliJ IDEA 2020.3 цей плагін включено автоматично.

Припустимо, створено клас City, якому передує декілька анотацій:

package ua.inf.iwanoff.java.advanced.fifth;

import lombok.*;

@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class City {
    private String name;
    private int population;
}

Фактично створюється клас, в якому є конструктор без параметрів, конструктор з параметрами, гетери та сетери для полів і перевизначена функція toString(). Все це з'явиться після обробки коду і виконання дій, передбачених анотаціями. Тому в функції main() можна скористатися всіма цими засобами:

package ua.inf.iwanoff.java.advanced.fifth;

public class Main {
    public static void main(String[] args) {
        City Kharkiv = new City("Kharkiv", 1419000);
        System.out.println(Kharkiv);
        City Kyiv = new City();
        Kyiv.setName("Kyiv");
        Kyiv.setPopulation(2884000);
        System.out.println(Kyiv.getName() + " " + Kyiv.getPopulation());
    }
}

Примітка. Перед виконанням програми у вікні IntelliJ IDEA з'явиться повідомлення "Lombok requires enabled annotation processing". Для того, щоб повідомлення не з'являлося кожного разу, коли здійснюється старт програми, необхідно натиснути кнопку з текстом "Enable annotation processing".

Код класу City можна спростити, застосувавши анотацію @Data, яка фактично включає декілька анотацій (@ToString, @EqualsAndHashCode, @Getter @Setter і @RequiredArgsConstructor). Анотації @AllArgsConstructor і @NoArgsConstructor слід додати окремо. Тепер наш код виглядатиме простіше:

package ua.inf.iwanoff.java.advanced.fifth;

import lombok.*;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class City {
    private String name;
    private int population;
}

Є також анотація @Value – це варіант @Data, який генерує клас без можливості зміни даних. Фактично ми отримуємо те, що пропонує тип record в новітніх версіях Java. Після використання анотації перед класом City

package ua.inf.iwanoff.java.advanced.fifth;

import lombok.*;

@Value
@AllArgsConstructor
public class City {
    private String name;
    private int population;
}

деякі дії над об'єктом не зможуть бути виконані:

City Kyiv = new City();      // Error
Kyiv.setName("Kyiv");        // Error   
Kyiv.setPopulation(2884000); // Error

Існують також анотації для складніших випадків, наприклад, @Cleanup, яка генерує конструкцію try-with-resources, @Builder, яка створює "будівельник" об'єктів, @Log, яка спрощує процес логування тощо.

2.6 Засоби Java для роботи з базами даних

2.6.1 Загальні концепції

Робота з базами даних – одна з найбільш розповсюджених задач, яка виникає практично на всіх рівнях застосування сучасних інформаційних технологій. Найбільш розповсюджений варіант баз даних – реляційні бази даних. Далі розглядатиметься робота саме з реляційними базами даних.

Є два підходи до реалізації застосунків баз даних, залежно від поставленої задачі:

  • створення реляційних таблиць, які відображають відношення між класами й об'єктами, які були створені раніше;
  • проєктування класів моделі, які відображають сутності, які зберігаються в раніше створених реляційних таблицях.

Платформа Java пропонує свої рішення для роботи з базами даних на різних рівнях:

  • JDBC, універсальне низькорівневе рішення;
  • фреймворки SQL, такі як jOOQ;
  • повномасштабні Об'єктно-реляційні відображення (Object Relational Mapping, ORM), такі як Hibernate або будь-яка інша реалізація JPA (Java Persistence API).

jOOQ Object Oriented Querying – бібліотека програмного забезпечення для відображення бази даних на Java, надає об'єкти для конструювання в коді SQL-записів із класів, згенерованих зі схеми бази даних.

Об'єктно-реляційне відображення дозволяє працювати з об'єктами класів, які відображають сутності, які зберігаються в базі даних.

2.6.2 Основи JDBC

JDBC (Java Database Connectivity) є стандартним інтерфейсом програмування застосунків баз даних (application programming interface, API). JDBC надає засоби організації доступу прикладних програм до баз даних, які не залежать від конкретної реалізації СУБД. JDBC API є частиною платформи Java SE. Класи та інтерфейси JDBC API містяться в пакетах java.sql і javax.sql.

JDBC забезпечує доступ до широкого кола СУБД, а також будь-яких табличних даних, наприклад, до електронних таблиць.

Набір класів та інтерфейсів JDBC, що входять до пакета пакет java.sql, надає можливості для здійснення таких дій:

  • встановлення з'єднання з базою даних;
  • створення та передачі базі даних SQL-запитів та команд;
  • обробка результатів.

Архітектура JDBC передбачає використання таких частин:

  • застосунок Java, який використовує засоби JDBC;
  • менеджер драйверів – компонент JDBC, який розподіляє клієнтські виклики по відповідних базах даних (JDBC Driver Manager);
  • драйвер JDBC, який підтримує сеанс зв'язку з конкретною базою даних (JDBC Driver),
  • база даних.

Менеджер драйверів JDBC гарантує, що для доступу до кожного джерела даних використовується відповідний драйвер. Нижче наводиться схема, яка показує розташування менеджера драйверів відносно драйвера JDBC і застосунку Java:

JDBC API підтримує як дворівневу, так і трирівневу моделі обробки доступу до бази даних.

Якщо драйвер JDBC постачається виробником бази даних, Java-додаток може отримати доступ до неї через механізм JDBC без посередників. Цей варіант JDBC дозволяє працювати у дворівневій архітектурі:

JDBC API надає низку інтерфейсів та класів.

Інтерфейс Driver відповідає за зв'язок із сервером бази даних. Драйвер JDBC можна завантажити так:
Class.forName("com.mysql.jdbc.Driver");

Під час завантаження відбувається автоматична реєстрація драйвера. Не можна змінити СУБД без перекомпіляції програми.

Клас DriverManager забезпечує ідентифікацію, вибір та завантаження драйвера баз даних залежно від зазначеного JDBC URL (Uniform Resource Locator – унікальний визначник розташування ресурсу), а також створення нових з'єднань з базою даних за допомогою методу getConnection(). URL, за допомогою якого JDBC ідентифікує базу даних, має такий синтаксис:

jdbc:<subprotocol>:<subname>

Тут <subprotocol> – тип системи бази даних, що використовується, <subname> залежить від субпротоколу, наприклад:

jdbc:sqlite:MyDataBaseName

Для мережевої URL-адреси рекомендується такий синтаксис:

//hostname:port/subsubname

Наприклад:

jdbc:mysql://localhost/MyDataBaseName
jdbc:mysql://localhost:3306/MyDataBaseName

Інтерфейс Connection надає широкий спектр методів зв'язку з базою даних – від завдання запитів SQL до обробки транзакцій.

Інтерфейси Statement, PreparedStatement і CallableStatement визначають методи передачі команд SQL базі даних. Об'єкт Statement, створений методом createStatement() класу Connection, використовується, коли команду SQL передбачається виконати один раз; об'єкт PreparedStatement, створений методом prepareStatement(String sql) об'єкта Connection, дозволяє команду SQL зберегти у прекомпільованому вигляді подальшого багаторазового використання, об'єкт CallableStatement – представити команду SQL як виклик збережених процедур.

Інтерфейс Statement надає три різні методи виконання команд SQL:

  • executeQuery(String sql) – для запитів, результатом яких є єдиний набір значень, наприклад для запитів SELECT;
  • executeUpdate(String sql) – для виконання команд INSERT, UPDATE або DELETE, а також для команд CREATE TABLE та DROP TABLE;
  • execute(String sql) – якщо оператори SQL повертають більше одного набору даних.

Інтерфейс PreparedStatement, похідний від Statement, та інтерфейс CallableStatement похідний від PreparedStatement, мають свої версії методів executeQuery(String sql), executeUpdate(String sql) і execute(String sql).

Інтерфейс ResultSet надає набір даних результату (result set), згенерованих відповідно до виконаної командою SQL. Набір даних результату являє собою таблицю із заголовками колонок та значеннями, що відповідають запиту SQL. Доступ до даних у рядках такої таблиці здійснюється за допомогою гетерів, які організовують доступ до колонок поточного рядка. Для переміщення до наступного рядка ResultSet використовується метод next(). Наприклад:

    java.sql.Statement stmt = conn.createStatement();
    ResultSet r = stmt.executeQuery("SELECT a, b, c FROM Table");
    while (r.next()) 
    {
        int i = r.getInt("a");
        String s = r.getString("b");
        double d = r.getDouble("c");
        System.out.printf(i + "\t" + s + "\t" + d);
    }

Також до складу пакету java.sql входять класи Date (специфікація SQL-типу DATE), Numeric (специфікація SQL-типів DECIMAL и NUMERIC) і Time (специфікація SQL-типу TIME).

2.6.3 Організація взаємодії СУБД із застосунком

За способом доступу бази даних можуть бути файл-серверними, клієнт-серверними та вбудованими.

У файл-серверних СУБД дані зберігаються у вигляді окремих файлів на сервері, СУБД розташовується на клієнтському комп'ютері, тому обробка даних відбувається на боці клієнта. Доступ СУБД до даних здійснюється через мережу, яка має досить великі навантаження. Така архітектура вважається недостатньо надійною та безпечною. Прикладами файл-серверних баз даних є Microsoft Access, Paradox, dBase, FoxPro.

Клієнт-серверна СУБД разом із базою даних розташовується на сервері. Обробка даних у цьому разі відбувається на сервері, локальна мережа потенційно менше завантажена. Можливе колективне використання такої бази даних, у якій забезпечується централізоване управління. Як правило, потребує спеціального встановлення. Така архітектура має високу надійність і безпеку. Приклади таких СУБД: Oracle, MS SQL Server, MySQL, IBM DB2, PostgreSQL, Firebird, H2.

Вбудована СУБД може розповсюджуватися як складова частина деякого програмного продукту. Вона працює на тому ж комп'ютері, що і прикладна програма, не вимагає процедури самостійної установки, реалізується у вигляді бібліотеки, що підключається. Вбудована СУБД призначена для локального зберігання даних програми. Застосовуються у випадку, якщо не потрібний доступ з багатьох комп'ютерів. Характеризується високою швидкістю та малою витратою пам'яті, проте максимальний розмір бази є відносно невеликим. Надійність і безпека досить високі, проте такі рішення щодо надійності поступаються клієнт-серверним СУБД. Приклади СУБД, що вбудовуються: SQLite, H2, BerkeleyDB, Firebird Embedded, Microsoft SQL Server Compact.

Деякі СУБД існують як у клієнт-серверному, так і в варіантах, що вбудовуються. Це, наприклад, бази даних Firebird (Firebird Embedded) , MySQL (MySQL Embedded Server Library), H2 тощо.

3 Приклади програм

3.1 Отримання даних про методи класу

Припустимо, необхідно створити програму, яка виводить дані про методи деякого класу – ім'я, тип результату і типи параметрів. Програма матиме такий вигляд:

package ua.inf.iwanoff.java.advanced.fifth;

import java.lang.reflect.Method;
import java.util.Scanner;

public class ShowAllMethods {

    @SuppressWarnings("resource")
    public static void main(String[] args) {
        System.out.print("Уведіть повне ім\'я класу: ");
        String className = new Scanner(System.in).next();
        try {
            Class<?> c = Class.forName(className);
            for (Method m : c.getMethods()) {
                System.out.printf("Ім'я: %s Тип результату: %s%n", m.getName(), m.getReturnType().getCanonicalName());
                for (Class<?> type : m.getParameterTypes()) {
                    System.out.printf("  Тип параметра: %s%n", type.getCanonicalName());
                }
            }
        }
        catch (ClassNotFoundException e) {
            System.err.println("Помилка введення імені класу!");
        }
    }

}
      

3.2 Обчислення значень функцій класу Math

У наведеному нижче прикладі функції getMethod() та invoke() використані для виклику статичних методів класу java.lang.Math (можуть бути викликані методи з одним параметром):

package ua.inf.iwanoff.java.advanced.fifth;

import java.lang.reflect.Method;
import java.util.InputMismatchException;
import java.util.Scanner;

public class SomeFunction {
    private Method method = null;
    private String methodName = null;
    private String className = null;

    public SomeFunction(String className, String methodName) {
        this.className = className;
        this.methodName = methodName;
    }

    public double getY(double x) throws Exception {
        if (method == null) {
            // Створюємо об'єкт типу Method, якщо його не було створено раніше.
            // Метод повинен приймати один аргумент типу double:
            method = Class.forName(className).getMethod(methodName, double.class);
        }
        // Викликаємо метод:
        return (Double) method.invoke(null, x);
    }

    @SuppressWarnings("resource")
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        System.out.println("Уведіть ім'я статичного методу класу Math:");
        SomeFunction someFunction = new SomeFunction("java.lang.Math", scanner.next());
        System.out.println("Уведіть початок, кінець інтервалу та крок:");
        try {
            double from = scanner.nextDouble();
            double to = scanner.nextDouble();
            double step = scanner.nextDouble();
            for (double x = from; x <= to; x += step) {
                System.out.println(x + "\t" + someFunction.getY(x));
            }
        }
        catch (InputMismatchException e) {
            System.out.println("Помилка введення даних");
        }
        catch (Exception e) {
            System.out.println("Помилка виклику функції");
        }
    }

}

3.3 Виведення інформації про методи, позначені певною анотацією

У наведеному нижче прикладі кілька класів завантажуються за іменами й для них викликаються методи без параметрів, помічені анотацією @ToInvoke. Визначаємо анотацію:

package ua.inf.iwanoff.java.advanced.fifth;

import java.lang.annotation.*;

@Target(value = ElementType.METHOD)
@Retention(value = RetentionPolicy.RUNTIME)
public @interface ToInvoke { }

У програмі створюємо два класи з методами, поміченими анотацією @ToInvoke і здійснюємо виклик:

package ua.inf.iwanoff.java.advanced.fifth;

import java.lang.reflect.*;

class ATest {

    public void aFirst() {
        System.out.println("aFirst launched");
    }

    @ToInvoke
    public void aSecond() {
        System.out.println("aSecond launched");
    }

    public void aThird() {
        System.out.println("aThird launched");
    }
}

class BTest {

    @ToInvoke
    public void bFirst() {
        System.out.println("bFirst launched");
    }

    public void bSecond() {
        System.out.println("bSecond launched");
    }

    @ToInvoke
    public void bThird() {
        System.out.println("bThird launched");
    }

}

public class LaunchIfAnnotated {

    static void invokeFromClass(String className) {
        System.out.println("---------- class " + className + " ----------");
        try {
            Class<?> cls = Class.forName(className);
            Method[] methods = cls.getMethods();
            for (Method m : methods) {
                if (m.isAnnotationPresent(ToInvoke.class)) {
                    m.invoke(cls.newInstance());
                }
                else {
                   System.out.println("No annotation before " + m.getName());
                }
            }
        }
        catch (ClassNotFoundException | SecurityException | 
               InstantiationException | IllegalAccessException |
               IllegalArgumentException | InvocationTargetException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        invokeFromClass("ua.inf.iwanoff.java.advanced.fifth.ATest");
        invokeFromClass("ua.inf.iwanoff.java.advanced.fifth.BTest");
    }

}

3.4 Застосунок баз даних для роботи з даними про переписи населення

В лабораторній роботі № 3 було створено Maven-проєкт, в якому здійснюється робота з даними про країну та переписи населення. Тепер цей проєкт можна розширити.

У консольному застосунку слід створити дві таблиці реляційної бази даних, відповідно для країн та переписів.

Після створення таблиць необхідно виконати такі дії:

  • імпорт в базу даних з JSON-файлу;
  • відображення даних обох таблиць;
  • здійснення сортування переписів певної країни за збільшенням населення;
  • додавання нового перепису до певної країни;
  • видалення перепису;
  • додавання нової країни;
  • пошук переписів з певним словом у коментарі;
  • експорт з бази даних в JSON-файл.

До проєкту додаємо новий пакет ua.inf.iwanoff.java.advanced.fifth. В ньому створюємо похідні класи для представлення перепису й країни, додавши необхідні конструктори. Нове поле id, додане в цих класах, може придатися під час взаємодії з базою даних. Клас CensusForDB буде таким:

package ua.inf.iwanoff.java.advanced.fifth;

import ua.inf.iwanoff.java.advanced.first.CensusWithStreams;

/**
 * Клас для представлення перепису населення.
 * Для потенційного зберігання в базі даних додано ідентифікатор
 */
public class CensusForDB extends CensusWithStreams {
    private long id = -1;

    /**
     * Повертає ID перепису
     *
     * @return ID перепису
     */
    public long getId() {
        return id;
    }

    /**
     * Встановлює ID перепису
     *
     * @param id ID перепису
     */
    public void setId(long id) {
        this.id = id;
    }

    /**
     * Конструктор ініціалізує об'єкт усталеними значеннями
     */
    public CensusForDB() {
    }

    /**
     * Конструктор ініціалізує об'єкт вказаними значеннями
     *
     * @param year       рік перепису
     * @param population кількість населення
     * @param comments   текст коментаря
     */
    public CensusForDB(int year, int population, String comments) {
        super(year, population, comments);
    }
}

Код класу CountryForDB буде таким:

package ua.inf.iwanoff.java.advanced.fifth;

import ua.inf.iwanoff.java.advanced.first.CountryWithStreams;

/**
 * Клас для представлення країни, в якій здійснюється перепис населення.
 * Для потенційного зберігання в базі даних додано ідентифікатор
 */
public class CountryForDB extends CountryWithStreams {
    private long id = -1;

    /**
     * Повертає ID країни
     *
     * @return ID країни
     */
    public long getId() {
        return id;
    }

    /**
     * Встановлює ID країни
     *
     * @param id ID країни
     */
    public void setId(long id) {
        this.id = id;
    }

    /**
     * Конструктор ініціалізує об'єкт усталеними значеннями
     */
    public CountryForDB() {
    }

    /**
     * Конструктор ініціалізує об'єкт вказаними значеннями
     *
     * @param name назва країни
     * @param area територія країни
     */
    public CountryForDB(String name, double area) {
        setName(name);
        setArea(area);
    }

}

Оскільки йдеться про декілька країн, доцільно створити новий клас Countries, який містить список країн:

package ua.inf.iwanoff.java.advanced.fifth;

import java.util.List;
import java.util.ArrayList;

/**
 * Клас для представлення списку країн
 */
public class Countries {
    private List<CountryForDB> list = new ArrayList<>();

    /**
     * Повертає список країн (java.util.List)
     * @return список країн
     */
    public List<CountryForDB> getList() {
        return list;
    }

    /**
     * Надає подання об'єкта у вигляді рядка
     *
     * @return представлення списку у вигляді рядка
     */
    @Override
    public String toString() {
        return list.toString();
    }
}

Тепер слід перейти до налаштувань, пов'язаних з базою даних. Спочатку слід встановити сервер баз даних MySQL (якщо його не було встановлено раніше). Встановити клієнт-серверну версію СУБД MySQL для Windows можна за допомогою програми MySQL Installer (http://dev.mysql.com/downloads/installer/). Завантажити цю програму можна без реєстрації. Можна вибрати варіант mysql-installer-community-8.0.36.0.msi (версія може змінитися). На першій сторінці програми встановлення MySQL слід вибрати варіант конфігурації. Можна вибрати варіант Full, або Server only, що достатньо для виконання нашої роботи. Є також варіанти для досвідчених користувачів. Якщо вибрати варіант Server only, на наступній сторінці слід натиснути кнопку Execute. Починається процес встановлення сервера. Далі слід двічі натиснути Next, на сторінці Type and Networking можна все залишити без змін. Далі слід знову двічі натиснути Next, ввести пароль. В реальній практиці слід вживати достатньо складні паролі, але для нашої роботи ми встановимо пароль root. На наступних сторінках можна нічого не змінювати. На сторінці Apply Configuration слід натиснути кнопку Execute, потім Finish. На наступних сторінках необхідно підтвердити факт встановлення MySQL.

До файлу pom.xml треба додати таку залежність:

<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.33</version>
</dependency>

Основна робота виконуватиметься у класі DbUtils:

package ua.inf.iwanoff.java.advanced.fifth;

import com.thoughtworks.xstream.XStream;
import com.thoughtworks.xstream.io.json.JettisonMappedXmlDriver;
import com.thoughtworks.xstream.security.AnyTypePermission;

import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.sql.*;
import java.util.ArrayList;

/**
 * Клас реалізує роботу з базою даних в задачі обробки даних про переписи населення.
 * Здійснюється робота з сервером баз даних MySQL
 */
public class DbUtils {
    /**
     * Перелік для визначення порядку виведення даних про переписи
     */
    public enum Show { SORTED, UNSORTED };

    // Константи, які містять необхідні SQL-запити:
    public static final String DROP_TABLES = "DROP TABLES IF EXISTS censuses, countries";
    public static final String DROP_DATABASE = "DROP DATABASE IF EXISTS countriesDB";
    public static final String CREATE_DATABASE = "CREATE DATABASE countriesDB";
    public static final String CREATE_TABLE_COUNTRIES = """
            CREATE TABLE countriesDB.countries (
              CountryID INT NOT NULL AUTO_INCREMENT,
              Name VARCHAR(128) NULL,
              Area DECIMAL(10,2) NULL,
              PRIMARY KEY (CountryID));
            """;
    public static final String CREATE_TABLE_CENSUSES = """
            CREATE TABLE countriesDB.censuses (
              CensusID INT NOT NULL AUTO_INCREMENT,
              Year INT NULL,
              Population INT NULL,
              CountryID INT NULL,
              Comment VARCHAR(256) NULL,
              PRIMARY KEY (CensusID),
              INDEX CountryID_idx (CountryID ASC) VISIBLE,
              CONSTRAINT CountryID
                FOREIGN KEY (CountryID)
                REFERENCES countriesDB.countries (CountryID)
                ON DELETE NO ACTION
                ON UPDATE NO ACTION);                        
          
            """;
    private static final String INSERT_INTO_COUNTRIES = """
        INSERT INTO countriesDB.countries (Name, Area) VALUES (?, ?);
        """;
    private static final String INSERT_INTO_CENSUSES = """
        INSERT INTO countriesDB.censuses (Year, Population, CountryID, Comment) VALUES (?, ?, ?, ?);
        """;
    private static final String SELECT_BY_NAME = "SELECT * FROM countriesDB.countries WHERE Name = ?";
    private static final String SELECT_ALL_COUNTRIES = "SELECT * FROM countriesDB.countries";
    private static final String SELECT_FROM_CENSUSES = "SELECT * FROM countriesDB.censuses WHERE CountryID = ?";
    private static final String SELECT_FROM_CENSUSES_ORDER_BY_POPULATION =
            "SELECT * FROM countriesDB.censuses WHERE CountryID = ? ORDER BY Population";
    private static final String SELECT_FROM_CENSUSES_WHERE_WORD = """
             SELECT c.CensusID, c.Year, c.Population, c.Comment, l.Name FROM countriesDB.censuses c 
             INNER JOIN countriesDB.countries l ON c.CountryID = l.CountryID WHERE c.Comment LIKE '%key_word%';
        """;
    private static final String DELETE_BY_YEAR = "DELETE FROM countriesDB.censuses WHERE CountryID=? AND Year=?";

    private static Connection connection;

    /**
     * Здійснює десеріалізацію даних про країни зі вказаного XML-файлу
     *
     * @param fileName ім'я файлу
     * @return об'єкт, який було створено
     */
    public static Countries importFromJSON(String fileName) {
        try {
            XStream xStream = new XStream(new JettisonMappedXmlDriver());
            xStream.addPermission(AnyTypePermission.ANY);
            xStream.alias("countries", Countries.class);
            xStream.alias("country", CountryForDB.class);
            xStream.alias("census", CensusForDB.class);
            return (Countries) xStream.fromXML(new File(fileName));
        }
        catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * Здійснює серіалізацію даних про країни у вказаний JSON-файл
     *
     * @param countries країни
     * @param fileName ім'я файлу
     */
    public static void exportToJSON(Countries countries, String fileName) {
        XStream xStream = new XStream(new JettisonMappedXmlDriver());
        xStream.alias("countries", Countries.class);
        xStream.alias("country", CountryForDB.class);
        xStream.alias("census", CensusForDB.class);
        String xml = xStream.toXML(countries);
        try (FileWriter fw = new FileWriter(fileName); PrintWriter out = new PrintWriter(fw)) {
            out.println(xml);
        }
        catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * Здійснює серіалізацію даних про країни з бази даних у вказаний JSON-файл
     *
     * @param fileName ім'я файлу
     */
    public static void exportToJSON(String fileName) {
        Countries countries = getCountriesFromDB();
        exportToJSON(countries, fileName);
    }

    /**
     * Створення з'єднання з базою даних
     */
    public static void createConnection() {
        try {
            connection = DriverManager.getConnection("jdbc:mysql://localhost/mysql?user=root&password=root");
        }
        catch (SQLException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * Створення бази даних і таблиць з видаленням попередніх
     */
    public static boolean createDatabase() {
        try {
            Statement statement = connection.createStatement();
            statement.executeUpdate(DROP_TABLES);
            statement.executeUpdate(DROP_DATABASE);
            statement.executeUpdate(CREATE_DATABASE);
            statement.executeUpdate(CREATE_TABLE_COUNTRIES);
            statement.executeUpdate(CREATE_TABLE_CENSUSES);
            return true;
        }
        catch (SQLException e) {
            return false;
        }
    }

    /**
     * Занесення всіх даних з об'єкту до бази даних
     *
     * @param countries об'єкт з даними про країни
     */
    public static void addAll(Countries countries) {
        for (CountryForDB c : countries.getList()) {
            addCountry(c);
        }
    }

    /**
     * Занесення даних про країну до бази даних
     *
     * @param country країна, дані про яку заносяться
     */
    public static void addCountry(CountryForDB country) {
        try {
            PreparedStatement preparedStatement = connection.prepareStatement(INSERT_INTO_COUNTRIES);
            preparedStatement.setString(1, country.getName());
            preparedStatement.setDouble(2, country.getArea());
            preparedStatement.execute();
            for (int i = 0; i < country.censusesCount(); i++) {
                addCensus(country.getName(), (CensusForDB) country.getCensus(i));
            }
        }
        catch (SQLException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * Створює об'єкт Countries, в який заносить всі дані з бази
     *
     * @return об'єкт, в який занесені дані про країни
     */
    public static Countries getCountriesFromDB() {
        try {
            Countries countries = new Countries();
            Statement statement = connection.createStatement();
            ResultSet setOfCountries = statement.executeQuery(SELECT_ALL_COUNTRIES);
            while (setOfCountries.next()) {
                countries.getList().add(getCountryFromDB(setOfCountries));
            }
            return countries;
        }
        catch (SQLException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * Створює об'єкт-країну, заповнюючи його даними з бази
     *
     * @param name назва країни
     * @return об'єкт, заповнений даними з бази
     */
    public static CountryForDB getCountryByName(String name) {
        try {
            PreparedStatement preparedStatement = connection.prepareStatement(SELECT_BY_NAME);
            preparedStatement.setString(1, name);
            ResultSet setOfCountries = preparedStatement.executeQuery();
            setOfCountries.next();
            return getCountryFromDB(setOfCountries);
        }
        catch (SQLException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * Створює та повертає об'єкт-країну з набору даних результату
     *
     * @param setOfCountries набір даних результату, з якого здійснюється отримання даних про країну
     * @return створена країна з даними про переписи
     * @throws SQLException виняток, пов'язаний з помилкою SQL-запиту
     */
    public static CountryForDB getCountryFromDB(ResultSet setOfCountries) throws SQLException {
        CountryForDB country = new CountryForDB(setOfCountries.getString("Name"), setOfCountries.getDouble("Area"));
        int id = setOfCountries.getInt("CountryID");
        country.setId(id);
        PreparedStatement preparedStatement = connection.prepareStatement(SELECT_FROM_CENSUSES);
        preparedStatement.setInt(1, id);
        ResultSet setOfCensuses = preparedStatement.executeQuery();
        while (setOfCensuses.next()) {
            CensusForDB census = new CensusForDB(setOfCensuses.getInt("Year"),
                    setOfCensuses.getInt("Population"), setOfCensuses.getString("Comment"));
            census.setId(setOfCensuses.getInt("CensusID"));
            country.addCensus(census);
        }
        return country;
    }

    /**
     * Отримує ID країни за назвою
     *
     * @param countryName назва країни
     * @return ID країни в базі даних
     */
    public static int getIdByName(String countryName) {
        try {
            PreparedStatement preparedStatement = connection.prepareStatement(SELECT_BY_NAME);
            preparedStatement.setString(1, countryName);
            ResultSet resultSet = preparedStatement.executeQuery();
            resultSet.next();
            return resultSet.getInt("CountryID");
        }
        catch (SQLException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * Виводить на консоль всі дані з бази, послідовно для кожної країни
     */
    public static void showAll() {
        try {
            PreparedStatement preparedStatement = connection.prepareStatement(SELECT_ALL_COUNTRIES);
            ResultSet resultSet = preparedStatement.executeQuery();
            ArrayList<String> names = new ArrayList<>();
            while (resultSet.next()) {
                String name = resultSet.getString("Name");
                names.add(name);
            }
            resultSet.close();
            for (String name : names) {
                showCountry(name, Show.UNSORTED);
            }
        }
        catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * Виводить на консоль дані про країни за назвою
     *
     * @param countryName назва країни
     * @param byPopulation порядок виведення, визначений переліком Show
     */
    public static void showCountry(String countryName, Show byPopulation) {
        try {
            PreparedStatement preparedStatement = connection.prepareStatement(SELECT_BY_NAME);
            preparedStatement.setString(1, countryName);
            ResultSet resultSet = preparedStatement.executeQuery();
            System.out.printf("%s\t  %s\t  %s%n", "ID", "Країна", "Територія");
            resultSet.next();
            System.out.printf("%s\t  %s\t  %s%n", resultSet.getString("CountryID"),
                    resultSet.getString("Name"), resultSet.getString("Area"));
            resultSet.close();
            PreparedStatement anotherStatement;
            if (byPopulation == Show.SORTED) {
                anotherStatement = connection.prepareStatement(SELECT_FROM_CENSUSES_ORDER_BY_POPULATION);
            }
            else {
                anotherStatement = connection.prepareStatement(SELECT_FROM_CENSUSES);
            }
            anotherStatement.setInt(1, getIdByName(countryName));
            ResultSet anotherSet = anotherStatement.executeQuery();
            System.out.printf("%s\t  %s\t  %s \t%s%n", "ID", "Рік", "Населення", "Коментарі");
            while (anotherSet.next()) {
                System.out.printf("%s\t  %s\t  %s\t\t%s%n",
                        anotherSet.getString("CensusID"), anotherSet.getString("Year"),
                        anotherSet.getString("Population"), anotherSet.getString("Comment"));
            }
        }
        catch (SQLException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * Виводить дані про переписи, відсортовані за збільшенням населення
     *
     * @param countryName країна, для якої виводяться переписи
     */
    public static void showSortedByPopulation(String countryName) {
        showCountry(countryName, Show.SORTED);
    }

    /**
     * Виводить на консоль дані про всі переписи, в коментарях до яких міститься певне слово
     *
     * @param word слово для пошуку
     */
    public static void findWord(String word) {
        try {
            String query = SELECT_FROM_CENSUSES_WHERE_WORD.replace("key_word", word);
            PreparedStatement preparedStatement = connection.prepareStatement(query);
            ResultSet resultSet = preparedStatement.executeQuery();
            while (resultSet.next()) {
                System.out.printf("%s\t  %s\t  %s\t  %s\t\t%s%n",
                        resultSet.getString("CensusID"), resultSet.getString("Name"),
                        resultSet.getString("Year"), resultSet.getString("Population"), resultSet.getString("Comment"));
            }
        }
        catch (SQLException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * Додає до бази даних інформацію про перепис конкретної країни
     *
     * @param countryName назва країни, перепис якої додається
     * @param census перепис, який додається
     */
    public static void addCensus(String countryName, CensusForDB census) {
        CountryForDB country = getCountryByName(countryName);
        try {
            PreparedStatement preparedStatement = connection.prepareStatement(INSERT_INTO_CENSUSES);
            preparedStatement.setInt(1, census.getYear());
            preparedStatement.setInt(2, census.getPopulation());
            preparedStatement.setInt(3, getIdByName(country.getName()));
            preparedStatement.setString(4, census.getComments());
            preparedStatement.execute();
        }
        catch (SQLException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * Видаляє вказаний перепис з бази даних
     *
     * @param countryName назва країни
     * @param year рік перепису, який слід видалити
     */
    public static void removeCensus(String countryName, int year) {
        try {
            PreparedStatement preparedStatement = connection.prepareStatement(DELETE_BY_YEAR);
            preparedStatement.setInt(1, getIdByName(countryName));
            preparedStatement.setInt(2, year);
            preparedStatement.execute();
        }
        catch (SQLException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * Закриває зв'язок з базою даних
     */
    public static void closeConnection() {
        try {
            connection.close();
        }
        catch (SQLException e) {
            throw new RuntimeException(e);
        }
    }
}

В класі з функцією main() додаємо також функцію для створення необхідних об'єктів з занесенням даних у файл JSON. З цього файлу потім здійснюється читання даних і занесення їх у базу даних, потім виконуються всі заплановані дії над даними:

package ua.inf.iwanoff.java.advanced.fifth;

import static ua.inf.iwanoff.java.advanced.fifth.DbUtils.*;

/**
 * Клас демонструє роботу з базою даних MySQL.
 * Здійснюється робота з даними про країни та переписи
 */
public class DbProgram {
    /**
     * Демонстрація роботи програми.
     * Дані імпортуються з JSON-файлу, здійснюється сортування, додавання та видалення.
     * Результат експортується в інший JSON-файл
     *
     * @param args аргументи командного рядка (не використовуються)
     */
    public static void main(String[] args) {
        Countries countries = createCountries();
        exportToJSON(countries, "Countries.json");
        countries = importFromJSON("Countries.json");
        createConnection();
        if (createDatabase()) {
            addAll(countries);
            showAll();
            System.out.println("\nПереписи в Україні за зростанням населення:");
            showSortedByPopulation("Україна");
            System.out.println("\nДодаємо перепис:");
            addCensus("Україна", new CensusForDB(2023, 49000000, "Може, буде?"));
            showAll();
            System.out.println("\nВидаляємо перепис:");
            removeCensus("Україна", 2023);
            showAll();
            System.out.println("\nДодаємо країну:");
            addCountry(new CountryForDB("Німеччина", 357588));
            addCensus("Німеччина", new CensusForDB(2011, 80200000, "Перший перепис у об'єднаній Німеччині"));
            showAll();
            System.out.println("\nПошук слова \"перепис\":");
            findWord("перепис");
            exportToJSON("CountriesFromDB.json");
            closeConnection();
        }
    }

    /**
     * Створення об'єкта Countries і заповнення даними для демонстрації програми
     *
     * @return об'єкт, який містить необхідні дані для демонстрації програми
     */
    static Countries createCountries() {
        CountryForDB country = new CountryForDB();
        country.setName("Україна");
        country.setArea(603628);
        country.addCensus(new CensusForDB(1959, 41869000, "Перший післявоєнний перепис"));
        country.addCensus(new CensusForDB(1970, 47126500, "Нас побільшало"));
        country.addCensus(new CensusForDB(1979, 49754600, "Просто перепис"));
        country.addCensus(new CensusForDB(1989, 51706700, "Останній радянський перепис"));
        country.addCensus(new CensusForDB(2001, 48475100, "Перший перепис у незалежній Україні"));
        Countries countries = new Countries();
        countries.getList().add(country);
        return countries;
    }
}

Результатом роботи буде виведення на консоль маніпуляцій з даними, а також новий файл CountriesFromDB.json.

4 Вправи для контролю

  1. Створити консольний застосунок, в якому користувач вводить ім'я класу й отримує інформацію про всі відкриті поля цього класу.
  2. Створити клас Student, згенерувавши необхідні методи за допомогою засобів Lombok.
  3. Створити клас Student з даними тільки для читання, згенерувавши необхідні методи за допомогою засобів Lombok.

5 Контрольні запитання

  1. У чому суть RTTI?
  2. Для чого використовується ключове слово instanceof?
  3. Дайте визначення рефлексії.
  4. Що за інформація зберігається в полі class?
  5. Як здійснюється завантаження класів в Java?
  6. Які існують види завантажувачів класів?
  7. Як завантажити клас за ім'ям?
  8. Для чого створюються користувацькі завантажувачі?
  9. Як отримати інформацію про методи класу?
  10. Як викликати метод за ім'ям?
  11. Як викликати статичний метод?
  12. Як отримати інформацію про поля класу?
  13. Навіщо використовуються анотації?
  14. Які існують стандартні анотації?
  15. Як наявність інструкції впливає виконання анотованого методу?
  16. Як визначаються характеристики анотації?
  17. Як задається час життя анотації?
  18. Як створити власну анотацію?
  19. Як перевірити наявність анотації?
  20. Як отримати перелік усіх анотацій?
  21. Які можливості надає бібліотека Lombok?
  22. Які є підходи до реалізації застосунків баз даних, залежно від поставленої задачі?
  23. Дайте визначення JDBC.
  24. Що таке драйвер JDBC?
  25. Чим відрізняються методи виконання команд SQL?

 

up