Расширяем диалекты через … аспекты.

Говорят (только не говорите этого женщинам), что в мире нет совершенства. Но почему-то все к нему стремятся. Вот и получается, что при разработке продуктов, которые должны уметь работать с различными СУБД (если не одновременно, то хотя бы в зависимости от опций развертывания), нам приходится учитывать все эти стремления их авторов к абсолютному и безусловному совершенству. Наверное, исходя из того, что клин клином вышибают, известный всем и уважаемый мною господин Gavin King добавил в любимый народом Hibernate поддержку различных SQL-диалектов, и даже сделал этот набор расширяемым.

Теперь представим ситуацию: мы разрабатываем продукт, который должен уметь работать с несколькими различными СУБД, например MySQL версий 4.x и 5.x, ну и postgresql. Для диалектов этих СУБД Hibernate предоставляет несколько классов - MySQLDialect, MySQLInnoDBDialect, MySQLMyISAMDialect, MySQL5Dialect, MySQL5InnoDBDialect и, наконец, PostgreSQL. Однако ни в одном из них не поддерживаются, например, побитовые операции. А что делать, если они нужны? Авторы рекомендуют расширить стандартные классы и в конструкторе каждого из них добавить в диалект реализацию недостающих функций. Т.е. в нашем случае нужно добавить шесть классов. А что делать, если количество планируемых к поддержке СУБД больше? Классический пример неудачно спроектированной иерархии, когда метаданные статически проецируют в поведение.

Однако кто вам сказал, что в мире нет совершенства? Наверное, он забыл вас предупредить, что есть AOP, и ApectJ как инструмент его реализации применительно к java. C помощью одного нехитрого аспекта можно расширить все используемые в продукте диалекты.
import org.aspectj.lang.annotation.AfterReturning; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.SuppressAjWarnings; import org.hibernate.Hibernate; import org.hibernate.dialect.Dialect; import org.hibernate.dialect.function.SQLFunctionTemplate; import java.util.Map; /** * @author Alexander Gavrilov */ @Aspect public class ExtendDialectsAspect { @SuppressWarnings({"unchecked"}) @SuppressAjWarnings("adviceDidNotMatch") @AfterReturning(value = "initialization(org.hibernate.dialect.MySQLDialect+.new(..)) && this(dialect)", argNames = "dialect") public void extendMySQLDialects(Dialect dialect) { addBitwise(dialect.getFunctions()); } @SuppressWarnings({"unchecked"}) @SuppressAjWarnings("adviceDidNotMatch") @AfterReturning(value = "initialization(org.hibernate.dialect.PostgreSQLDialect+.new(..)) && this(dialect)", argNames = "dialect") public void extendPostgreSQLDialect(Dialect dialect) { addBitwise(dialect.getFunctions()); } protected void addBitwise(Map functions) { functions.put("bit_or", new SQLFunctionTemplate(Hibernate.INTEGER, "?1 | ?2")); functions.put("bit_not", new SQLFunctionTemplate(Hibernate.INTEGER, "~?1")); } }

Теоретически, в данном конкретном случае, можно было бы отрефакторить все в один advice и изменить его pointcut так, что бы он описывал точки создания или MySQLDialect или PostgreSQLDialect, однако я решил оставить задел на будущее - вдруг придется расширить диалекты функциями, синтаксис которых у MySQL и PostreSQL различается. Не трудно заметить, что добавление поддержки еще одного диалекта выливается в добавление всего лишь одного advice-метода в класс. А то, что расширения всех диалектов сосредоточены в одном месте, является неоспоримым плюсом в долгосрочной перспективе нелегкого труда по сопровождению нашего совершенного кода в этом далеком от совершенства мире ;)

P.S. В следующий раз я планирую рассказать о том, как я компилирую аспекты средствами IntelliJ IDEA+ или maven2+, а так же когда и как использую LTW и CTW

 

2 replies


  1. “Эт все харашо, да. Все праильн, да. Все праильн.” (c)

    А как это дебажить то? ;)


  2. А что тут дебажить то? Hibernate дебажить? Или эти пару строк?
    А вообще надо писать интеграционные тесты!

Leave a reply