Реформа SQL

您所在的位置:网站首页 clientID Реформа SQL

Реформа SQL

2023-04-10 06:48| 来源: 网络整理| 查看: 265

Вводная Мне часто в проектах приходится сталкиваться с фреймворками по работе с БД. Концептуально, эти фреймворки можно разбить на 2 больших класса:

ORM-ориентированные SQL-ориентированные Некоторые из них хороши, какие-то не очень. Но субъективно могу сказать: SQL-ориентированные уступают в развитии ORM-ориентированным. Подчеркну, в развитии, а не в возможностях. Хоть изменить эту чашу весов и не получится, но предложить необычный взгляд на мир SQL-ориентированного подхода — вполне. Кому интересно, добро пожаловать под кат Немного обзора Прежде чем преступить, предлагаю вспомнить, совсем кратко, что есть. Кто в теме, смело пропускайте обзор.

И так, на сцене представители ORM-ориентированного подхода:

Hibernate EclipseLink MyBatis JOOQ Эти представители справляются со своей задачей по-разному. Последний, например, DSL-way, предлагает конструировать SQL запросы, используя, либо сгенерированные объекты по схеме Вашей БД, либо просто строки. Другие, требуют описывать соответствие между java-объектами и таблицами БД. Но не суть. Их все объединяет одна идея: максимально изолировать разработчика от написания SQL-запросов, предлагая взамен — ORM-мышление.

С другой стороны собрались представители SQL-ориентированного подхода:

Spring Framework JDBC sql2o (очень понравился) JDBI RefOrms Все эти решения в том или ином виде являются надстройками над пакетом java.sql.*. И тем эти фреймворки круче, чем сильнее изолируют разработчика от него. Тем не менее, используя их, мыслить Вам придется сначала категориями SQL, а потом уж ORM.

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

Что не так? Я умышленно не давал оценочных суждений тому или иному подходу, ORM vs SQL vs Генераторы. Каждый решает сам, в купе с имеющимися обстоятельствами, что выбирать. Но вот готов предложить определенную альтернативу как в стилистическом выражении так и в концептуальном для SQL-ориентированных. Но прежде скажу, что мне не нравится на уровне кода (производительность, debug и прочее — опускаю) в существующих решениях:

Определенная многословность для достижения простых вещей Бойлерплейт, бойлерплейт, бойлерплейт… и еще раз бойлерплейт Отсутсвие точки в коде, где можно посмотреть sql-orm или orm-sql варианты В том или ином виде конструирование SQL-запроса по условиям фильтрации Многознание для использования API фреймворка — узнай про +100500 сущностей прежде, чем преступить кодить Многое из перечисленного заставило спросить: 'А каким фреймворк должен быть, чтобы тебе понравилось?'

Декларативный стиль Каким? Думаю простым, таким, чтобы взял и начал кодить. Но, а если серьезно? Декларативным. Да, я сторонник декларативного стиля в таких вещах, нежели императивного. Что в java приходит на ум первым, если речь идет о декларативном подходе? Да всего 2 вещи: аннотации и интерфейсы. Если эти 2 субстанции скрестить и направить в русло SQL-ориентированного решения, то получим следующее:

ОРМpublic class Client { private Long id; private String name; private ClientState state; private Date regTime; public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public ClientState getState() { return state; } public void setState(ClientState state) { this.state = state; } public Date getRegTime() { return regTime; } public void setRegTime(Date regTime) { this.regTime = regTime; } } enum ClientState { ACTIVE(1), BLOCKED(2), DELETED(3); private int state; ClientState(int state) { this.state = state; } @TargetMethod public int getState() { return state; } @TargetMethod public static ClientState getClientState(int state) { return values()[state - 1]; // только для краткости } } public interface IClientDao { @TargetQuery(query = "SELECT id, name, state " + " FROM clients " + " WHERE id = ?", type = QT_SELECT) Client findClient(long clientId); } Табличка-- Таблица клиентов CREATE TABLE clients ( id bigint NOT NULL, name character varying(127) NOT NULL, state int NULL, reg_time timestamp NOT NULL, CONSTRAINT pk_clients PRIMARY KEY (id) ); Это простой рабочий пример, суть которого подчеркнуть идею декларативного стиля, которая находит отражение в рассматриваемом фреймворке. Сама идея разумеется не нова, заметки о похожем я встречал в статьях IBM где-то за 2006 год, а некоторые приведенные фреймворки уже используют эту идею. Но увидев такой пример, я бы резонно задал несколько вопросов:

А кто реализует контракт IClientDao и как получить доступ к реализации? А где описывается меппинг полей? А как насчет чего-то посложнее? А то эти примеры на 3 копейки уже надоели. Предлагаю по порядку ответить на эти вопросы и походу раскрыть возможности фреймворка.

Просто Proxy >>1. А кто реализует этот контракт и как получить доступ к реализации?

Контракт реализуется самим фреймворком с помощью инструмента java.lang.reflect.Proxy и реализовывать его самому для SQL целей не нужно. А получить доступ к реализации очень просто, с помощью… Да в прочим на примере показать легче:

IClientDao clientDao = com.reforms.orm.OrmDao.createDao(connection, IClientDao.class); Client client = clientDao.findClient(1L); Где connection — это объект для доступа к БД, например, реализация java.sql.Connection или javax.sql.DataSource или вообще Ваш объект. Client — Ваш ORM объект и пожалуй единственная вещь от самого фреймворка — это класс com.reforms.orm.OrmDao, который покроет 98% всех Ваших нужд.

Концепция >>2. А где описывается меппинг полей?

Как и обещал выше, я затрону 2 вещи: стиль и концепцию. Чтобы ответить на второй вопрос, нужно рассказать о концепции. Посыл таков, что, чтобы предложить новое — нужен радикальный взгляд на решение. А как насчет SQL-92 парсера? Когда мне впервые пришла эта мысль, я откинул ее так далеко, что думал больше с ней не встречусь. Но как тогда сделать SQL-ориентированный фреймворк удобным? Пилить очередную надстройку? Или делать очередной хелпер к хелперу фреймворка? На мой взгляд, лучше ограничить поддерживаемый набор SQL конструкций — как неплохой компромисс, и взамен получить нечто удобное, на мой взгляд. Мэппинг осуществляется на основе дерева выражений, после парсинга SQL-запроса. В примере выше имена колонок соотносятся с именами полей ORM объекта один к одному. Разумеется, фреймворк поддерживает меппинг и более сложный, но о нем чуть позже.

Примеры >> 3. А как насчет чего-то посложнее? А то эти примеры на 3 копейки уже надоели.

А есть ли смысл вообще заморачиваться с SQL-92 парсером, если фреймворк не будет уметь делать, чего-то посложнее? Но показать все в примерах без объема — не простая задача. Разумеется я покажу, правда буду опускать местами декларации таблиц в SQL и части java-кода.

Одной из немногих вещей, которая мне никогда не нравилась в SQL-ориентированных решениях — это необходимость конструирования SQL-запросов. Например, когда определенные критерии фильтрации могут быть заданы, а могут быть и не заданы. И Вам наверняка знаком такой фрагмент кода, точнее его упрощенный вариант:

// где-то в DAO private String makeRegTimeFilter(Date beginDate, Date endDate) { StringBuilder filter = new StringBuilder(); if (beginDate != null) { filter.append(" reg_time >= ?"); } if (endDate != null) { if (filter.length() != 0) { filter.append(" AND"); } filter.append(" reg_time < ?"); } return filter.length() == 0 ? null : filter.toString(); } И написал я этот фрагмент именно так, как чаще всего встречаю в старых проектах. И это при том, что проверки дат появятся еще раз при установке значений в PreparedStatement. А что предлагается наш друг? А предлагает он динамические фильтры. На примере разбираться легче, поэтому посмотрим на поиск клиентов за определенный интервал:

public interface IClientDao { @TargetQuery(query = "SELECT id, name, state " + " FROM clients " + " WHERE regTime >= ::begin_date AND " + " regTime < ::end_date", type = QT_SELECT, orm = Client.class) List findClients(@TargetFilter("begin_date") Date beginDate, @TargetFilter("end_date") Date endDate); } И суть такова, что если значение параметра, например, beginDate будет равно null, то ассоциированный с ним SQL-фильтр regTime >= ::begin_date будет вырезан из финального SQL-запроса и в нашем случае на сервер БД уйдет следующая строка:

SELECT id, name, state FROM clients WHERE regTime < ? Если оба значения будут равны null, то секции WHERE не будет в итоговом запросе. И заметьте — в коде только декларация и никакой логики. На мой взгляд, но очень хочется послушать и других, это сильное оружие и сильная сторона фреймворка. По java коду скажу, что я сам не фанат аннотаций и большое их количество в большинстве проектов просто раздражает. Поэтому предусмотрел определенную им альтернативу — фильтры по свойствам bean объекта:

// Бойлерплейт get/set код вырезал. В своем файле. public class ClientFilter { private Date beginDate; private Date endDate; } public interface IClientDao { @TargetQuery(query = "SELECT id, name, state " + " FROM clients " + " WHERE regTime >= ::begin_date AND " + " regTime < ::end_date", type = QT_SELECT, orm = Client.class) List findClients(@TargetFilter ClientFilter period); } Получается довольно лаконично и понятно. Про динамические фильтры стоит отдельно сказать, что они поддерживают, точнее они могут быть вырезанными из подзапросов и всех предикатов, исключая OVERLAPS и MATCH. Последние я ни разу не встречал в 'живых' SQL выражениях, но упоминание о них в спецификации SQL-92 имеется.

Разумеется, фреймворк поддерживает и статические, обязательные к указанию фильтры. И синтаксис у них такой же, как в HQL или SpringTemplate — ': именованный параметр'.

Правда, со статическими фильтрами всегда связана одна проблема: 'Как реагировать на null параметр'? Быстрым ответом кажется сказать — 'Кидай исключение, не ошибешься'. Но всегда ли это нужно? Давайте перейдем к примеру, загрузки клиентов с определенным статусом и проверим это:

public interface IClientDao { @TargetQuery(query = "SELECT id, name, state " + " FROM clients " + " WHERE state = :state", type = QT_SELECT, orm = Client.class) List findClients(@TargetFilter("state") ClientState state); } Но вот задача, что делать, если статус клиента в БД может отсутствовать? Колонка state допускает NULL значения и нам нужен поиск именно таких клиентов, безстатусников? Концепция SQL-92 парсера спасает вновь. Достаточно заменить выражение фильтрации по статусу с ':state' на ':state?' как движок фреймворка модифицирует секцию WHERE в следующий вид '… WHERE state IS NULL', если, конечно, на вход методу падать null.

Про меппинг Фильтры в запросах это конечно хорошо, но в java решениях большое внимание уделяется мэппингу результата работы SQL-запроса на ORM объекты и их связывание и связывание самих сущностей. Этого настолько много, посмотрите чего стоит одна спецификация JPA. Или посмотрите в своих проектах на перекладывания из ResultSet в объекты предметной области или установку значений в PreparedStatement. Громоздко, не правда ли? Я выбрал путь, может меннее надежный и менее изящный, но однозначно — простой. Зачем далеко ходить, когда можно устроить меппинг прям в SQL-запросе. Это ведь первое, что приходит в голову?

Давайте сразу к примерам. И вот задача, как мепить результат запроса, если все колонки таблицы отличаются от полей ORM класса, плюс ORM имеет вложенный объект?

ORM классыpublic class Client { private long clientId; private String clientName; private ClientState clientState; private Address address; private Date regTime; // ниже как обычно... } enum ClientState { ACTIVE(1), BLOCKED(2), DELETED(3); private int state; ClientState(int state) { this.state = state; } @TargetMethod public int getState() { return state; } @TargetMethod public static ClientState getClientState(int state) { return values()[state - 1]; // для примера сойдет } } public class Address { private long addressId; private String refCity; private String refStreet; } public interface IClientDao { @TargetQuery(query = "SELECT cl.id AS client_id, " + // underscore_case -> camelCase преобразование " cl.name AS clientName, " + // можно сразу в camelCase " cl.state AS client_state, " + // any_type to enum преобразование автоматически поддерживается, если в enum имеется аннотация @TargetMethod " cl.regDate AS t#regTime, " + // t# - это директива, что нам нужна и дата и время java.sql.Timestamp -> java.util.Date " addr.addressId AS address.addressId, " + // обращение к вложенному объекту через точку, а как еще? " addr.city AS address.refCity, " + " addr.street AS address.refStreet " + " FROM clients cl, addresses addr" + " WHERE id = :client_id", // допускается указание именованного параметра для простого фильтра type = QT_SELECT) Client findClient(long clientId); } Этот пример более преближен к реальным условиям. Эстетичность меппинга определенно хромает, но все же мне ближе этот вариант, чем бесконечные аннотации или xml файлы. Да, с надежностью беда, здесь и runtime и вопросы рефакторинга ORM объектов и не всегда возможность взять SQL и протестировать в своем любимом БД-клиенте. Но я не скажу, что это безвыходная ситуация — от runtime и рефакторинга спасают тесты. Чтобы запрос проверить в БД-клиенте придется его 'почистить'. Еще момент: Какой SQL-запрос уйдет на сервер БД? Все секции AS будут вырезаны из запроса. Если же потребуется сохранить, например, для client_id значение cid в качестве алиаса, то нужно добавить перед этим алиасом двоеточие: cl.id AS cid:client_id и cid будет жить -> cl.id AS cid в финальном запросе.

И последнее, немного бизнес логики Идеальные дао, когда одна операция — один декларативный метод. И это конечно хорошо, но так бывает не всегда. В приложении часто нужно получить гибридную или составную сущность, когда часть данных формируется одним SQL-запросом, другая часть вторым SQL-запросом и т.д. Или сделать определенные склейки, проверки. Возможно это и не суть самого дао, но такие манипуляции как правило имеют место быть и их изолируют, но делают публичными, дабы вызывать из разных участков программы. Но что нам делать с интерфейсами, если все же приспичит засунуть бизнес логику в дао? Неожиданно для меня и к удивлению многих разработчиков в java8 появились дефолтные методы. Круто? Да я знаю, что новость протухла, ведь на дворе 2017 год, но не сыграть ли на этом? А что если скрестить декларативный стиль принятый за основу разработки DAO слоя и дефолтные методы для бизнес логики? А давайте посмортим, что из этого получится, если понадобиться добавить проверку ORM объекта на null и загрузить данные из другого DAO:

public interface IClientDao { // этот метод уже видели @TargetQuery(query = "SELECT id, name, state " + " FROM clients " + " WHERE id = ?", type = QT_SELECT) Client findClient(long clientId); // получить дао внутри другого дао доступно из коробки IAddressDao getAddressDao(); // метод с бизнес логикой default Client findClientAndCheck(long clientId, long addressId) throws Exception { Client client = findClient(clientId); if (client == null) { throw new Exception("Клиент с id '" + clientId + "' не найден"); } // Здесь код может быть сколь угодно сложным, если нужно IAddressDao addressDao = getAddressDao(); client.setAddress(addressDao.loadAddress(addressId)); return client; } } Не знаю, прав ли, но мне хочется назвать это — интерфейсным программированием. Как-то так. На самом деле и все, чего хотелось рассказать. Но это точно не все, что умеет фреймворк, помимо оговоренного: выборку ограниченного числа колонок, управление схемами, постраничную разбивку (не для всего правда), манипуляции с упорядочиванием данных, отчеты, обновление, встаку и удаление.

Заключение Не знаю дорогой читатель, удалось ли мне привнести чего-то нового, в этот заполненный мир SQL-фреймворков или нет, судить Вам. Но я попробовал и доволен тем, что попробовал. Жду с нетерпением критического взгляда на предложенный подход и идей, если таковые имеются. Решение доступно на гитхабе, ссылка на который уже указана в главе Немного обзора в списке SQL-ориентированных фреймворков последней строчкой. Всего хорошего.

Редакции N1. Добавил тип объекта Client.class в аннотацию @TargetQuery для методов, возвращающих списки.


【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3