[Из песочницы] Быстрая разработка CRUD на Java: дауншифтинг с «1С: Предприятие»
В связи с последними событиями на мировой арене и понижением курса национальной валюты, для программистов на «1С: Предприятие» наступают непростые времена. Многих увольняют, одновременно усиливается конкуренция со стороны новичков, которых на рынке появилось довольно много — на что не пожалуешься, так как, работая преподавателем в учебном центре при МГТУ им. Баумана, я и сам приложил к этому руку, выдавая свидетельства щедрой рукой.
Вместе с тем, открываются перспективы по освоению других языков, так как работа на зарубежного заказчика внезапно вновь стала выгодной. Также возрос интерес к открытому программному обеспечению на всех уровнях технологического стека, а больше всего, к “импортозамещающим” СУБД типа PostgreSQL, MySQL.
Оказавшись в очередной раз на межпроектной развилке, я получил немного свободного времени, чтобы рассказать о своем опыте реализации нескольких проектов на Java, и о том, каково оно было, после многих лет разработки на 1С. Смысл послушать есть хотя бы потому, что количество просмотров резюме Java разработчика по моим оценкам сейчас раз в 5 больше резюме 1Сника.
Рассказать хочу на примере 2 моих OpenSource проектов, выкладываемых на GitHub:
№1. Реализует базовую функциональность быстрой разработки, доступную в 1С.
№2. Реализует механизм формирования отчетов с пользовательскими настройками типа “сводная таблица”, упрощенный аналог СКД (системы компоновки данных в 1С).
Для начала, по первому проекту. Я начал создавать базу данных для одной организации. И очень скоро встретился с первыми препятствиями. Не то чтобы освоить банальный CRUD было так сложно — Java я знал, да дело и не в языке: витруозом родился — виртуозом и помрешь, хоть как Мусоргский запейся — гениальность не пропьешь.
Но… как выйти на прежнюю скорость разработки? В мелких 1С проектах приходится постоянно модифицировать базу данных, добавляя реквизиты, сущности, а требования клиентов часто меняются по ходу игры. И будучи “ленивым 1Сником” (с) я как-то привык, что добавив сущность или внеся изменения в сущность, можно нажать 1 (одну) кнопку, после чего произойдет реструктуризация базы данных, запуск программы, и изменения можно будет увидеть в форме списка, и в форме элемента, самостоятельно сгенерированных платформой. Если же реквизит ссылается на новый справочник, для него автоматически создадутся новые формы списка, выбора, элемента.
Что же можно сказать про Java… На самом деле, можно добавить реквизит в код класса — Eclipse дает возможность в полуавтоматическом режиме создать геттер и сеттер, а после упомянутой 1 кнопки (F11) база данных под ORM Hibernate действительно дополнится новой таблицей или новым столбцом (если включить hibernate.hbm2ddl.auto=update, хотя многие и против такого подхода — понятно, что на продакшне его выключим).
Давайте рассмотрим пример. Допустим, у нас был класс “Контакты”, и мы решили добавить в него реквизит “Статус контакта”, перечень которых будет храниться в отдельном справочнике, который надо сейчас же дать редактировать пользователю. Тогда вносим изменения в класс (изменения помечены “плюсами”, геттеры и сеттеры созданы Eclipse):
Table(name=«contacts»)
@Synonym(text=«Контакты»)
@Forms(element="")
public class Contacts implements java.io.Serializable {
@Synonym(text=«Код»)
private int id;
@Synonym(text=«Фамилия»)
private String f;
@Synonym(text=«Имя»)
private String i;
@Synonym(text=«Отчество»)
private String o;
@Synonym(text=«Статус») //++++++++++++++++++++++++++
private Contact_Status status; //++++++++++++++++++++++++++
@Synonym(text=«Адрес»)
private String address;
@Synonym(text=«Телефон»)
private String phone;
@Synonym(text=«Прочее»)
private String description;
public Contacts() {
}
public Contacts(int id, String f, String i, String o,
Contact_Status status, //++++++++++++++++++++++++++
String address, String phone, String description) {
this.id = id;
this.f = f;
this.i = i;
this.o = o;
this.status = status; //++++++++++++++++++++++++++
this.address = address;
this.phone = phone;
this.description = description;
}
Id
@GeneratedValue(strategy=GenerationType.IDENTITY)
@Column(name = «id»)
public int getId() {
return this.id;
}
public void setId(int id) {
this.id = id;
}
@Synonym(text=«Фамилия»)
public String getF() {
return this.f;
}
public void setF(String f) {
this.f = f;
}
public String getI() {
return this.i;
}
public void setI(String i) {
this.i = i;
}
public String getO() {
return this.o;
}
public void setO(String o) {
this.o = o;
}
//{++++++++++++++++++++++++++
@ManyToOne(targetEntity = Contact_Status.class,cascade={CascadeType.ALL}
NotFound(action=NotFoundAction.IGNORE)
@JoinColumn(name = «contact_status», referencedColumnName=«id»,nullable=true,insertable=false,updatable=true)
// by Eclipse
public Contact_Status getStatus() {
return this.status;
}
public void setStatus(Contact_Status status) {
this.status = status;
}
//}++++++++++++++++++++++++++
public String getAddress() {
return this.address;
}
public void setAddress(String address) {
this.address = address;
}
public String getPhone() {
return this.phone;
}
public void setPhone(String phone) {
this.phone = phone;
}
public String getDescription() {
return this.description;
}
public void setDescription(String description) {
this.description = description;
}
public String toString() {
return this.f+" "+this.i+" "+this.o;
}
}
Ну и добавляем саму сущность “Статусы”, простую как полено (я обычно копирую какую-нибудь и меняю названия):
Entity
Table(name=«contact_status»)
@Synonym(text=«Статусы»)
@Forms(element="")
public class Contact_Status implements java.io.Serializable {
@Synonym(text=«Код»)
private int id;
@Synonym(text=«Наименование»)
private String name;
Id
@GeneratedValue(strategy=GenerationType.IDENTITY)
@Column(name = «id»)
// by Eclipse
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String toString(){
return name;
}
}
//}++++++++++++++++++++++++++
Если отбросить код, созданный Eclipse, останется совсем немного, а так как работать с текстом быстрее, чем бегать по меню в Конфигураторе 1С: Предприятия, считаю, результат уже неплох. Это я про базу данных.
Но как же интерфейс?
В сообществе Java пользуется популярностью UI фреймворк Swing. Но все примеры под него выполняли конкретные задачи, универсального решения, которое можно было бы использовать везде, я почему-то не нашел.
Не было банально приличного редактора форм. NetBeans неплох, но блокирует настроечный код, что страшно раздражает, а в Eclipse плагин windowsBilder постоянно этот код перечитывает, и тормозит при этом неимоверно — да и вылетает частенько. Плюс внутри анонимных классов обработчиков, созданных им, остальные элементы формы не видны — гениальный ход. IDEA почему-то раздражала при подтормаживании еще больше — хотя казалось бы SSD на M.2 должен был решить подобные проблемы.
В итоге задача “добавь сущность и закинь ее на форму” требует очень много кода и времени, по сравнению с 1С.
Спросят: “много” — это сколько, не зажрались ли вы, батенька?
Отвечу. Проводя тренинги для РП, я в качестве предисловия к одному из блоков с нуля делал простейший контур складского учета за 4 минуты 35 секунд (4:35, Карл!), включая ввод в базу тестового примера из двух документов. Участники тренинга замеряли время, а потом я с пафосом спрашивал у них — если все так просто, куда же тратятся миллионные бюджеты?
Но я не представлял, как добиться сопоставимой скорости разработки готового приложения, даже используя шаблон проекта с уже подключенными драйверами СУБД (PostgreSQL, MySQL, Oracle DB или что там у вас), ORM Hibernate и Swing (на самом деле, Swingx — в чистом Swing нет приличных элементов типа JXTreeTable). Очевидно, шаблон должен включать мощные классы-настройщики. И я решил их сделать, на коленке.
Если проводить параллель с 1С, табличное поле, привязанная к реквизиту формы (например, динамическому списку), это JXTable или JXTreeTable, привязанное к так называемым TableModel. Недолго думая, я отнаследовался от стандартной табличной и древовидной модели, добавил класс сущности, с которой мы работаем и текст запроса:
public class BeanTableModel extends DefaultTableModel{
...
private Class beanClass; //Класс сущности, "Основная таблица" динамического списка 1С
private String qtext; //Текст запроса динамического списка 1С
...
}
Самое главное, чтобы JXTable могла вызывать метод getValueAt(int row,int col) у своей TableModel. Что же касается JXTreeTable, там это getValueAt(ArrayNode node,col), где node — public class ArrayNode extends DefaultMutableTreeTableNode — вершина дерева.
Согласно запросу qtext, данные вытягиваются из базы, это может происходить как сразу большим куском, так и по мере необходимости (при вызове getValueAt), с реализацией кеша и пейджирования. В выложенной демке пейджирования нет, оно реализуется через очередную добавленную аннотацию, например @Paging, и дополнение модельных классов буфером.
Вывод таблицы должен содержать эти данные, но ведь пользователю надо выводить ограниченный перечень колонок, с правильными именами, и определенной ширины. Как добиться чтобы русские названия были заданы по умолчанию?.. Для этого, при описании entity в дополнение к persistent аннотациям и пришлось завести свою аннотацию Synonym, которую вы уже видели в листинге (а версия на GitHub уже поддерживает он-лайн переключение между любым количеством языков путем указания text, textEng, textDe, и добавления в глобальных props элемента language “Eng”, “De”...).
Чтобы два раза не вставать, к Entity я добавил и название классов форм аннотацией Forms — element, list, select. Понятно, что это должны быть потомки JFrame.
Но задача стояла сделать, чтобы программа, подобно платформе 1С: Предприятие сама создавала дефолтные формы (иначе какие уж там 4:35 на новую сущность), поэтому в примере в классе Contacts анностация пустая — @Forms(element=""), хотя ранее ссылалась на класс формы VContacts.
Как уже догадались коллеги, создание колонок в BeanTableModel, равно как и создание реквизитов в автоматически создаваемой формах элементов наших сущностей, происходит с помощью рефлексии, то есть работы с метаданным сущности. Я сходу наткнулся на 2 способа перебирать элементы метаданных: обычным способом (в классе FormElement) и через Интроспектор (в моделях).
Итак, мы вывели данные в таблицу посредством модели. Можно сказать, такая таблица — основа формы списка или выбора. Теперь неплохо было бы реализовать ее поведение — открытие формы элемента (добавление и редактирование), удаление, выбор и прочее.
Связать действия кнопок на JToolbar с таблицей удалось крайне некрасивым способом — через сканирование элементов внутри общего родителя. Ужаснувшись, я вынес это в отдельный класс ut (от слова “утилиты”) — чтобы больше не видеть. Но цель была достигнута.
Мой велосипед позволил:
1. Продолжить пользоваться редактором форм для расположения элементов на форме.
2. Настроить поведение этих элементов небольшими блоками кода (здесь подошел бы xml, но мне не нравится большое количество файлов, я в них путаюсь):
ContactTable.setTreeTableModel(new BeanTreeTableModel("select * from contacts", null, Contacts.class, new ArrayNode(new Object[0])));
ArrayList<HashMap> tt = new ArrayList< >();
tt.add(ut.newHashMap(new ArrayList < >(Arrays.asList("name,title,width", "id", "№", 60))));
...
tt.add(ut.newHashMap(new ArrayList< >(Arrays.asList("name,title,width", "phone", "Телефон", 200))));
ut.TuneTreeColumns(ContactTable, tt);
ut.TuneToolbar(Contacts_toolBar, ContactTable);
А у полей ввода с кнопками выбора настройка в 1 строчку типа:
ut.linkFormObjectElements(FormElement.this,d.getName(),t1,t2);
3. Если лень, можно не создавать вообще никаких форм для второстепенных сущностей, с тем, чтобы программа делала их автоматически, на основе самих классов и аннотаций. И эти формы могут открываться, даже если там 20 связей, функционал CRUD “Выбрать, Добавить, Редактировать, Удалить, Обновить” будет работать. Значит, студент свою лабораторную работу с базой для библиотеки сможет сляпать за 10 минут не приходя в создание.
Что же дальше?.. Java предоставляет гораздо более обширные возможности, чем встроенный язык 1С, но тот более лаконичен. Так как большинство объектов (ArrayList, HashMap и прочие) одинаковы, в принципе даже можно попробовать написать некий интерпретатор, но гораздо больше мне интересно расширение языка запросов SQL разыменовыванием, итогами, и виртуальными таблицами. Очевидно, что такая задача решается уже не через ORM.
Во второй статье будет рассказываться про проект инструмента гибкого формирования пользовательских отчетов, что как раз созвучно с темой запросов, так что там про это будет рассказано подробнее.
До новых встреч.