Статьи

Аннотации и отражения Java: мощные строительные блоки для интерфейса СУБД

Аннотации — это функция Java, представленная в JDK5 (аналогичный механизм, атрибуты, также существует в .NET). Основная идея аннотаций — дать программисту возможность указывать всю необходимую информацию в одном месте. Рассмотрим «классический» подход: есть исходные файлы с программным кодом; отдельные файлы с документацией API, описывающей поведение этого кода; и дополнительные файлы в SQL или некотором другом формате, предоставляющие некоторую связанную информацию (необходимую, например, для интерфейса базы данных).

Проблема с этим подходом заключается в трудности сбора и управления (программно, а также ментально) всей необходимой информацией. Глядя на источник метода, разработчик может захотеть понять значение его параметров. Но это объясняется в файле документации, который затем необходимо открыть и найти.

Это неудобно, но, что еще хуже, информация в отдельных местах вскоре становится противоречивой: например, обновляются исходные коды программы, но не документация. И хотя расхождение между реализацией и документацией является плохим, но не критическим, несоответствие в других областях может быть фатальным, например, когда файл конфигурации объясняет формат класса, а затем новое поле добавляется в этот класс без обновления Файл конфигурации. С аннотациями эта информация может быть доступна в одном месте для обучения, обновлений и т. Д.

Хотя аннотации еще не получили широкого распространения, они играют весьма полезную роль в разработке интерфейса для системы управления базами данных. Централизуя информацию о форматах объектов, аннотации упрощают работу программиста и устраняют риск ошибок из-за расхождений в способе описания объектов (классов). Это особенно полезно в интерфейсе базы данных, потому что описания объектов обязательно сложны: базе данных требуется больше информации, чем доступно в обычном определении класса Java. Например, строковое поле может храниться в базе данных с использованием разных кодировок. Кроме того, для эффективного запроса к базе данных должны быть созданы индексы, и у программиста должен быть способ отмечать первичные и внешние ключи в классе Java.

Аннотации особенно полезны в качестве основы для интерфейса СУБД в сочетании с механизмом отражения Java, который позволяет приложению получать информацию о формате класса во время выполнения. Поскольку Java работает с объектами, одно из требований — указать формат объектов (классов) для механизма базы данных. В таких языках, как C / C ++, есть два распространенных способа сделать это:

  • Используйте специализированный компилятор или препроцессор, который анализирует источник приложения и извлекает информацию о классе, или
  • Требовать от программиста описания форматов классов — в этом случае эта работа может фактически выполняться дважды: один раз при объявлении классов в приложении и второй раз при описании их формата для СУБД

Но в Java отражение может позволить СУБД получать информацию о формате классов приложений во время выполнения, устраняя необходимость в дополнительном программном обеспечении (специализированном компиляторе или препроцессоре) или дополнительной работе.

Представление интерфейса СУБД на основе аннотаций в Java

В оставшейся части этой статьи показано, как можно использовать аннотации и отражения Java для обеспечения API базы данных, который является одновременно более простым в использовании и менее подверженным ошибкам, чем альтернативные подходы. В качестве примера он использует собственный интерфейс Java (JNI) для встроенной базы данных eXtremeDB http://www.mcobject.com/extremedb-jni , который предоставляет средства для этой системы баз данных, написанной на C, для использования из «чистого» Java-приложения. Полную информацию о синтаксисе и семантике аннотаций в Java можно найти по адресу http://www.developer.com/article.php/3556176 . Для этой статьи достаточно знать, что

  • Аннотации — это особый вид интерфейса Java
  • Аннотации представлены синтаксисом, в котором имени аннотации предшествует символ @
  • Аннотации могут быть связаны с типами, полями, методами, параметрами и объявлениями локальных переменных
  • Параметры аннотаций могут быть указаны с помощью ключевой нотации, а если параметр не указан, используется значение по умолчанию. Параметр «значение» по умолчанию может быть установлен без указания имени параметра
  • Аннотации могут быть получены с использованием API отражения во время выполнения

Первым шагом является определение базы данных с помощью объявлений классов в приложении Java. Java API eXtremeDB состоит из девяти аннотаций, но для этого относительно простого примера требуется всего четыре: @Index, @Indexable, @Indexes и @Key. Эти аннотации имеют следующие свойства:

@Показатель

Аннотация @Index указывает индекс для класса. На самом деле, существует более удобный способ определения индекса одного поля: использование аннотации @Indexable для конкретного поля (более подробно описано ниже). Однако необходимость составных индексов в нашем примере решается с помощью этой отдельной аннотации на уровне класса. Параметры ниже демонстрируют @Index с индексом B-Tree, но также можно использовать R-Tree, Patricia Trie, KD Tree или хеш-индекс.

Параметры:

String name ()
: имя индекса
boolean unique () по умолчанию false: является ли индекс уникальным или разрешает дублирование
int initSize () по умолчанию 1000: начальный размер для хэш-таблицы
Database.IndexType type () default Database.IndexType.BTree: тип указателя
Key [] keys () : список индексных ключей

Target: class

Пример:

       @Index(name="byName", unique=true, keys={@Key("lastName"), @Key("firstName")})       class Person {           String firstName;           String lastName;

@Indexable 

Эта аннотация помечает поле как индексируемое и предоставляет наиболее удобный способ определения индекса одного поля. Поскольку Java позволяет связать только одну аннотацию определенного типа с конкретным объявлением, поле можно включить только в один индекс с помощью аннотации @Indexable. Если приложение требует определения более одного индекса для поля, вместо него используется аннотация @Index.

Параметры:

boolean unique () по умолчанию false: независимо от того, является ли индекс уникальным или разрешено ли дублирование
логического объекта по убыванию () по умолчанию false: размещаются ли объекты в индексе в порядке убывания или в порядке возрастания ключа
int initSize ( ) по умолчанию 1000: начальный размер для хеш-таблицы
Тип Database.IndexType ()default Database.IndexType.BTree: тип индекса

Цель: поле

Пример:

       @Indexable       int age;

@Indexes

Эта аннотация указывает набор индексов классов. Как упоминалось выше, Java не допускает дублирования аннотаций, поэтому, если требуется несколько индексов, их определения должны быть «обернуты» в аннотацию @Indexes.

Параметры:

Index [] value () : список дескрипторов индекса

Цель: class

Пример:

       @Indexes({@Index(name="byName", keys={@Key("lastName"), @Key("firstName")}),                 @Index(name="byAddress", keys={@Key("address.city"), @Key("address.street")})}       class Employee {           String firstName;           String lastName;           Address address;           ...       }

@key

Аннотация @key указывает ключ индекса. Его назначение — определить пару ключевых свойств: имя индексированного поля и его порядок (например, «Фамилия, Имя» или «Имя, Фамилия») в индексе.

Параметры:

String value (): имя индексируемого поля
boolean убывающий () по умолчанию false: где объекты размещаются в индексе в порядке убывания или возрастания ключа

Target: class (параметр аннотации @Index)

Пример: см. Пример @ Индексная аннотация

Определение базы данных

Используя аннотации, перечисленные выше, базу данных можно создать в приложении Java, определив следующие классы:

        class Record {@Indexablepublic int id; // record identifierpublic String str; // some string value        }  @Indexes({@Index(name="byName", keys={@Key("lastName"), @Key("firstName")}),              @Index(name=”byFullName”,keys={@Key(“fullName”)})})        class Person        {              public String firstName;              public String lastName;              public String fullName;              public int age;              public float weight;        }

Когда приложение, содержащее это определение, запускается, JNI использует отражение для проверки классов Java (и их аннотаций). Он использует эту информацию для создания словаря базы данных или централизованного хранилища метаданных, таких как типы данных, связи, индексы и многое другое. Отражение позволяет среде выполнения базы данных «знать», сколько места выделить для объекта, какие поля у объекта (и их типы), поля, участвующие в разных индексах, и многое другое.

Для разработчиков, привыкших работать на Java, этот подход обеспечивает значительное удобство. Альтернатива — будь то на основе собственного JNI, предоставляемого для СУБД, или основанного на стандартах интерфейса, такого как Java Database Connectivity (JDBC), — состоит в изучении языка определения данных, или DDL, который отличается от Java; создание документа схемы на этом языке вручную; обработка этой схемы с использованием языкового процессора или интерпретатора СУБД; и обеспечение того, чтобы результирующий файл словаря базы данных был доступен для процессов приложения, которым это необходимо.

Доступ к базе данных

В eXtremeDB JNI аннотации используются только для определения базы данных. В процедурном коде эти аннотации не играют никакой роли, и разработчик работает с объектами базы данных, как если бы они были Простыми старыми объектами Java (POJO), а это именно то, что предпочитают большинство поклонников Java. Следующие примеры показывают, как база данных, определенная выше, доступна в коде приложения Java:

Создание экземпляра базы данных:

        Database db = new Database();        Database.Parameters params = new Database.Parameters();        params.memPageSize = PAGE_SIZE;        params.classes = new Class[] { Record.class };        db.open("operations-db", params, DATABASE_SIZE);

Открытие соединения с базой данных:

       Connection con = new Connection(db);

Вставка данных в базу данных:

       // start Read-Write transaction       con.startTransaction(Database.TransactionType.ReadWrite);       Record  rec = new Record(); // create Java object       // fill data       rec.id = 1;       rec.str = "Some value";       con.insert(rec); // insert object into eXtremeDB database       con.commitTransaction(); // commit changes

Запрос к базе данных:

      // open read-only transaction      con.startTransaction(Database.TransactionType.ReadOnly);      // Open cursor by "id" index      Cursor<Record> cursor = new Cursor<Record>(con, Record.class, "id");      for (Record rec : cursor) { // print out all objects          System.out.println("id=" + rec.id + ", str=\"" + rec.str + "\"");      }      cursor.close(); // close cursor      con.commitTransaction(); // end transaction

Обновление объекта:

      con.startTransaction(Database.TransactionType.ReadWrite);      // Perform simple index search: locate Record by id      cursor = new Cursor<Record>(con, Record.class, "id");      // find record to update       rec = cursor.find(2);      // update object      rec.str = "Updated string";      cursor.update(); // update current object (pointed by the cursor) in database      cursor.close(); //release cursor      con.commitTransaction(); // commit changes

Удаление объекта:

      con.startTransaction(Database.TransactionType.ReadWrite);      // Perform simple index search: locate Record by id      cursor = new Cursor<Record>(con, Record.class, "id");      // find record with id == 3      rec = cursor.find(3);      cursor.remove(); // remove current object (pointed by cursor) from database      cursor.close(); //release cursor      con.commitTransaction(); // commit changes

Удаление всех объектов:

      con.startTransaction(Database.TransactionType.ReadWrite);      con.removeAll(Record.class);      con.commitTransaction();      // cleanup      con.disconnect();      db.close();

API на основе аннотаций и соединение с базой данных Java (JDBC)

В некоторых местах приведенный выше код иллюстрирует главное преимущество JNI на основе аннотаций по сравнению с такими подходами, как Java Database Connectivity (JDBC): при использовании JNI программисту не нужно реализовывать функции упаковки / распаковки. Вместо этого строка кода, такая как

                   Record rec = cursor.find("Some-Key");

это все, что нужно для получения полностью заполненного экземпляра ‘rec’. JNI извлечет все значения полей rec без явной распаковки.

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

С JDBC, выбор объекта из базы данных будет выглядеть примерно так:

    List<Person> findPersons(Connection con, String name)     {        Statement stmt = con.prepareStatement("select * from Person where full_name=?");        stmt.setString(1, name);        ResultSet cursor = stmt.executeQuery();        ArrayList<Person> persons = new ArrayList<Person>();        while (cursor.next()) {             Person person = new Person();            person.firstName = cursor.getString("first_name")            person.lastName = cursor.getString("last_name")            person.fullName = cursor.getString("full_name")            person.age = cursor.getInt("age")            person.weight = cursor.getFloat("weight")            persons.add(person);        }        cursor.close();        stmt.close();        return persons;    }

Однако база данных JNI, основанная на аннотациях и отражениях, позволяет кодировать одну и ту же функцию следующим образом:

    List<Person> findPersons(Connection con, String name)     {        con.startTransaction(Database.TransactionType.ReadOnly); // open read-only transaction        Cursor<Person> cursor = new Cursor<Person(con, Person.class, "byFullName");        ArrayList<Person> persons = new ArrayList<Person>();        if (cursor.search(Cursor.Operation.Equals, name)) {            for (Person person : cursor) {                persons.add(person);            }        }        cursor.close(); // close cursor        con.commitTransaction(); // end transaction        return list;    }

Обратите внимание, что код JDBC должен извлекать (копировать) каждое из полей объекта из курсора базы данных (т. Е. «Сопоставлять» строку реляционной БД с классом Java, как в объектно-реляционном отображении). Если поля класса Java не определены как имеющие один и тот же тип, или одно или несколько полей пропущены, возникает несоответствие между классом Java и таблицей RDBMS. Например, переменная может быть определена как число с плавающей запятой в СУБД и как целое число в классе Java. Эта потеря точности / точности, скорее всего, возникнет как ошибка до выпуска программного обеспечения и увеличит общее время разработки. Если ошибка каким-то образом проскальзывает через QA и превращается в рабочий код, потеря точности может быть более или менее серьезной в зависимости от функции и цели приложения. Например,высокая степень точности была бы желательна в таких контекстах, как банковский баланс или расчет «расстояние до пустого» или «время до цели». Использование языка Java (с аннотациями) в качестве языка определения данных полностью устраняет этот риск.

Аннотации для специализированных функций базы данных

Приведенный выше пример демонстрирует, как аннотации могут использоваться для определения индексов и ключей, которые будут использоваться для организации, сортировки и поиска объектов, содержащихся в классе Java. Подход может быть расширен для определения практически любой характеристики базы данных в объявлении класса Java. Следующие аннотации, опять-таки из JNI McObject eXtremeDB, иллюстрируют, как API на основе аннотаций решает более специализированные проблемы с базами данных.

Задача 1: Поддержка различных национальных схем кодирования

Строка Java состоит из двухбайтовых символов (кодировка UTF-16). eXtremeDB может хранить строки в виде последовательности символов (байтов) или в виде строк широких символов (двухбайтовых). По умолчанию eXtremeDB JNI использует кодировку UTF-8 для преобразования строк Java в последовательности байтов. С помощью аннотации @Encoding разработчик может указать любую национальную кодировку:

@Encoding параметры:

String value (): строковое кодирование, например «UTF-8», «ASCII», «Windows-1251», …

Цель: поле

Пример :

       @Encoding("UTF-8")       String name;

Указание кодировки «UTF-16» в аннотации приводит к тому, что строка сохраняется в eXtremeDB без какого-либо преобразования в виде последовательности широких символов.

Такой подход к аннотации также позволяет наиболее эффективно хранить строки на определенных языках — например, если я храню строки на русском языке в UTF-8, то каждый символ кириллицы будет храниться в виде двух байтов. Но если я использую кодировку «windows-1251», то каждый символ потребляет один байт в базе данных.

Задача 2. Обработка больших объектов данных

Массивы и строки eXtremeDB ограничены 65535 элементами. Для хранения больших объемов данных используются большие двоичные объекты. Хотя было бы возможно определить тип BLOB в Java API eXtremeDB, более удобно разрешить программисту использовать стандартные массивы Java или строковые типы в определении класса и использовать аннотацию @Blob, чтобы сообщить eXtremeDB, что это поле должен храниться как BLOB в базе данных.

Параметры @Blob: нет

Цель: поле

Пример:

       @Blob       byte body[]; 

Этот подход сохраняет значение поля в базе данных как BLOB.

Задача 3: Управление хранением в памяти, на диске и комбинированным (гибридным) хранением данных

. Традиционная СУБД временно кэширует данные в памяти, но в конечном итоге записывает обновленные записи на диск. eXtremeDB был разработан как система баз данных в памяти (IMDS), которая хранит записи в основной памяти, где они могут быть доступны непосредственно приложением. IMDS никогда не отправляются на диск с целью (и результатом) повышения производительности за счет устранения различных типов накладных расходов, включая дисковый и файловый ввод-вывод, обработку кэша и передачу данных.

Позднее McObject представила eXtremeDB Fusion, гибридную СУБД, которая поддерживает хранение в памяти и на диске в одном экземпляре базы данных. Такая гибкость хранения данных позволяет разработчикам находить компромиссы, которые могут повлиять на производительность, постоянство, стоимость оборудования и даже форм-фактор. Гибридное хранилище также полезно, когда в линейку продуктов входят одни устройства с жесткими дисками, а другие — без. В этом сценарии (обычно в цифровых телевизионных приставках, например) гибридная СУБД может значительно облегчить перенос программного кода через линейку продуктов.

С родным языком определения базы данных eXtremeDB Fusion, для определения одного набора данных как переходного (управляемого в памяти), при выборе хранения на диске для других типов записей, требуется простое объявление схемы. Собственный интерфейс Java выполняет то же самое — и даже больше — с помощью аннотации @Persistent, которая позволяет разработчику «переключаться» между хранением на диске и в памяти, большой или компактной компоновкой данных и реализацией других параметров, которые переопределяют настройки по умолчанию для хранения.

@Persistent параметры:

boolean list () default false: создать индекс списка для этого класса
boolean autoid () default false: назначить AUTOID экземплярам этого класса
boolean disk () default false: этот класс хранится на диске
boolean inmemory () по умолчанию false: этот класс предназначен только для оперативной памяти.
boolean compact () по умолчанию false: использовать компактную компоновку для этого класса (размер экземпляра этого класса не может превышать 64 КБ).
boolean large () по умолчанию false: использовать стандартную компоновку для этого класса (размер экземпляра этого класса может превышать 64 КБ)

Примечание:  inmemory () / ondisk () и compact () / large () служат для переопределения значений класса Database.Parameters для compactClassesByDefault и diskClassesByDefault.

Цель: класс

Пример:

       @Persistent(disk=true, list=true)       class MyClass {           ...       }

Вывод

Вместе отражения и аннотации Java предоставляют мощные строительные блоки для интерфейса базы данных. В случае eXtremeDB JNI относительно небольшой набор аннотаций (всего их девять) использует практически все функции СУБД. Хотя база данных написана на C и доступна главным образом, до сих пор, из приложений C и C ++, JNI позволяет разработчику Java оставаться в среде Java и устраняет необходимость создавать и обрабатывать файл схемы внешней базы данных или писать сериализацию. код (с сопутствующим риском внесения ошибок). Между тем, приложение Java получает явное преимущество в производительности от команд базы данных, которые выполняются со скоростью скомпилированного кода C, а не интерпретируемой Java.

Преимущества достаточно убедительны, чтобы искать другие проблемы с доступом к базе данных, где может применяться этот общий подход. Один из ответов заключается в предоставлении доступа к СУБД, такой как eXtremeDB, из .NET, которая поддерживает рефлексию и, как упоминалось выше, предлагает аналог аннотаций, называемых атрибутами. В отличие от Java, который имеет свой собственный Java Native API для взаимодействия между виртуальной машиной Java и собственным кодом, .NET позволяет вызывать практически любую внешнюю функцию. Казалось бы, это может упростить создание такого интерфейса, но работа также может включать выход за пределы «безопасной» области управляемого кода .NET и арифметику указателей неуправляемого кода, прямой доступ к памяти и другие «рискованные» аспекты , Создание интерфейса потребовало бы ознакомления с правилами объявления и вызова методов в .NET,все же значительные объемы кода JNI, вероятно, могут быть повторно использованы. Учитывая большое и растущее число разработчиков, ориентированных на .NET и C #, это кажется многообещающим направлением для СУБД, стремящихся увеличить свое присутствие в приложениях Windows.