Для пользователей explain.tensor.ru - нашего сервиса визуализации PostgreSQL-планов, мы создали плагин "Explain PostgreSQL" для всех IDE от JetBrains, теперь есть возможность форматировать запросы и анализировать планы непосредственно в IDE.
Как использовать плагин и детали о его разработке читайте ниже.
Установка плагина
К сожалению, попытка опубликовать плагин в JetBrains Marketplace завершилась отказом:
На основании имеющейся у нас информации, Вы являетесь лицом, связанным с организацией, находящейся в Российской Федерации, и/или с Правительством Российской Федерации.
По этой причине, а также в связи с недавно опубликованной поправкой к санкционному законодательству ЕС (8-й пакет санкций ЕС), JetBrains не может предоставлять Вам услуги ИТ‑консалтинга, включая техническую поддержку или помощь. Проверки и публикация плагинов на Маркетплейсе JetBrains попадают под определение предоставления технической поддержки.
Поэтому установку плагина надо делать из локального файла - скачиваем его с сайта explain.tensor.ru (раздел Download / Plugins ) и в Settings > Plugins
выбираем Install plugin from Disk .
Подключение к БД
создаем новый datasource:
скачиваем JDBC-драйвера PostgreSQL и подключаемся к БД:
Форматирование запроса
в консоли набираем текст запроса и нажимаем Ctrl-Q F или в контекстном меню SQL Format:
Анализ запроса
В контекстном меню выбираем Explain Plan > Explain Analyze (Tensor) , при этом выполнится запрос EXPLAIN (ANALYZE, BUFFERS)
полученный план отправится в сервис explain.tensor.ru через публичное API , результат откроется в новом окне:
Для IDE без плагина Database Tools
В WebStorm для работы с БД требуется установка плагина Database Tools and SQL for WebStorm . Если этот плагин не установлен или у вас community-версия IDE то открыть сайт explain.tensor.ru можно прямо в IDE:
Настройка плагина
Для тех кто развернул сервис локально, используя вариант self hosted, или использует сайт explain-postgresql.com , можно поменять сайт в настройках IDE в Tools > Explain PostgreSQL:
О разработке плагина для JetBrains IDE
Создаем новый проект "IDE plugin".
В окне есть ссылка на официальную документацию IntelliJ Platform SDK , в ней приведены основные сведения, используемые при разработке.
Создание Tool Window
создаем класс, реализующий интерфейс ToolWindowFactory и переопределяем метод createToolWindowContent , который будет вызываться при нажатии на кнопку окна - здесь в окне создается новая вкладка с сайтом explain.tensor.ru и кнопка новой вкладки:
public class ExplainToolWindowFactory implements ToolWindowFactory, DumbAware {
@Override
public void createToolWindowContent(@NotNull Project project, @NotNull ToolWindow toolWindow) {
createNewTab(project);
ButtonNewTab newTabBtn = new ButtonNewTab("New Tab", null, AllIcons.Toolbar.AddSlot);
((ToolWindowEx) toolWindow).setTabActions(newTabBtn);
}
public static void createNewTab(Project project) {
ApplicationManager.getApplication().invokeLater((new Runnable() {
@Override
public void run() {
ExplainBrowser browser = new ExplainBrowser(true);
ToolWindow toolWindow = ToolWindowManager.getInstance(project).getToolWindow(explainWindow);
Content content = ContentFactory.getInstance().createContent(browser.getComponent(), "Explain", false);
content.setDisposer(browser);
@NotNull ContentManager contentManager = toolWindow.getContentManager();
contentManager.addContent(content);
contentManager.setSelectedContent(content);
}
}));
}
}
-
кнопка создания новой вкладки - наследуем AnActionButton и переопределяем методы:
actionPerformed - выполняется при нажатии на кнопку, здесь мы создаем новую вкладку или открываем новое окно браузера если нажали вместе с Ctrl
updateButton - выполняется для определения доступности кнопки, в нашем случае если JDK с поддержкой JCEF браузера и открыто tool window
public class ButtonNewTab extends AnActionButton {
public ButtonNewTab(String text, String descr, Icon icon) {
super(text, descr, icon);
}
@Override
public void actionPerformed(@NotNull AnActionEvent e) {
if (e.getInputEvent().isControlDown()) {
BrowserUtil.browse(AppSettingsState.getInstance().getExplainUrl());
} else {
ExplainToolWindowFactory.createNewTab(e.getProject());
}
}
@Override
public void updateButton(@NotNull AnActionEvent e) {
super.updateButton(e);
boolean enabled = ExplainBrowser.isSupported && ExplainToolWindowFactory.getToolWindow(e.getProject()).isVisible();
e.getPresentation().setEnabled(enabled);
}
}
-
окно с JCEF-браузером
JCEF - это реализация Chromium Embedded Framework в Java, определяем возможность его использования методом isSupported и если нет, то вместо компонента с окном браузера выдаем сообщение:
public class ExplainBrowser implements Disposable {
public static final String EXPLAIN_URL = "https://explain.tensor.ru";
public static final boolean isSupported = JBCefApp.isSupported();
private JBCefBrowser browser = null;
private JPanel panel = null;
public ExplainBrowser(boolean loadOnStart) {
if (isSupported) {
browser = new JBCefBrowser(EXPLAIN_URL);
} else {
panel = new JPanel(new BorderLayout());
JLabel label = new JLabel("JCEF browser is not supported");
panel.add(label, BorderLayout.CENTER);
}
}
public JComponent getComponent() {
if (browser == null) {
return panel;
} else {
return browser.getComponent();
}
}
}
добавляем описание tool window в plugin.xml в разделе extensions, в параметре factoryClass указываем созданный в п.1 класс. IDE при старте прочитает plugin.xml и создаст наше окно снизу на панели инструментов
<extensions defaultExtensionNs="com.intellij">
<toolWindow factoryClass="com.mgorkov.toolwindow.ExplainToolWindowFactory"
id="Explain PostgreSQL"
anchor="bottom"
secondary="true"
canCloseContents="true"
largeStripeAnchor="true"
icon="AllIcons.Xml.Browsers.Chrome"
doNotActivateOnStart="true"
/>
</extensions>
Форматирование запроса
Создаем класс SQLFormatter и наследуем от AnAction , в нем также как и в AnActionButton надо переопределить методы actionPerformed и update.
В actionPerformed мы получаем текст документа и, используя API explain.tensor.ru , форматируем и заменяем исходный текст.
public class SQLFormatter extends AnAction {
private Document document = null;
private String formatted = null;
@Override
public void actionPerformed(@NotNull AnActionEvent e) {
Editor editor = e.getData(CommonDataKeys.EDITOR);
if (editor != null) {
document = editor.getDocument();
String text = document.getText();
formatted = explainApi.beautifier(e, text).join();
if (formatted != null) {
WriteCommandAction.runWriteCommandAction(e.getProject(), () -> {
document.deleteString(0, document.getTextLength());
document.insertString(0, formatted);
});
}
}
}
}
Чтобы IDE узнала об этом действии нужно зарегистрировать его в plugin.xml в разделе actions , указав реализующий класс, место размещения и комбинацию клавиш. Здесь мы размещаем действие в EditorPopupMenu и оно доступно при нажатии Ctrl Q F
<actions>
<action id="SQLFormat" class="com.mgorkov.actions.SQLFormatter" text="SQL Format">
<add-to-group group-id="EditorPopupMenu" anchor="last"/>
<keyboard-shortcut keymap="$default" first-keystroke="ctrl Q" second-keystroke="F"/>
</action>
</actions>
При наличии синтаксических ошибок выдается предупреждение:
Это реализовано в методе showErrorMessage сервиса ExplainApiService:
private void showErrorMessage(@NotNull AnActionEvent e, String msg) {
ApplicationManager.getApplication().invokeLater(() -> {
HintManager.getInstance().showErrorHint(Objects.requireNonNull(e.getData(CommonDataKeys.EDITOR)), msg);
});
}
Анализ запроса
Для выполнение запросов к базе используем плагин JetBrains Database Tools, он встроен во все IDE , кроме community edition и WebStorm (можно установить отдельно).
Здесь мы наследуем класс ExplainActionBase.Ui и в нем переопределяем метод explainStatement , в котором выполняем запрос и выводим окно браузера с результатом анализа в новой вкладке редактора:
FileEditor[] editors = FileEditorManager.getInstance(console.getProject()).openFile(file, true);
Arrays.stream(editors).filter(e -> e instanceof PgPlanEditor).forEach(e -> {
PgPlanEditor editor = (PgPlanEditor) e;
editor.getExplainWindow().updatePlan(mdl.getJson());
});
Далее регистрируем новые действия в контекстном меню JDBC-консоли. Чтобы плагин мог работать в IDE без Database Tools добавим в plugin.xml опциональную зависимость от него:
<depends optional="true" config-file="plugin-withdatabase.xml">com.intellij.database</depends>
и создадим plugin-withdatabase.xml :
<actions>
<action id="com.mgorkov.actions.ExplainAnalyzeAction"
class="com.mgorkov.actions.ExplainAnalyzeAction" text="Explain Analyze (Tensor)"
icon="DatabaseIcons.ConsoleShowPlan">
<add-to-group group-id="Console.Jdbc.ExplainGroup" anchor="last"/>
</action>
<action id="com.mgorkov.actions.ExplainAction" class="com.mgorkov.actions.ExplainAction"
text="Explain Plan (Tensor)"
icon="DatabaseIcons.ConsoleShowPlan">
<add-to-group group-id="Console.Jdbc.ExplainGroup" anchor="last"/>
</action>
</actions>
Настройка плагина
В IDE есть понятие сервисов, это компоненты плагина, которые загружаются по требованию при вызове getService() . При этом сервисы реализуют паттерн singleton, т.е. при вызове getService() всегда возвращается один и тот же компонент. Хранение настроек плагина реализуется сервисом типа applicationService (всего их три).
Реализация настроек выполнена по схеме MVC.
Роль модели выполняет класс AppSettingsState, реализующий PersistentStateComponent, при этом в аннотации надо указать имя параметра и файла, в котором сохраняются настройки:
@State(
name = "com.mgorkov.settings.AppSettingsState",
storages = @Storage("ExplainPostgreSQLSettingsPlugin.xml")
)
public final class AppSettingsState implements PersistentStateComponent<AppSettingsState> {
private String ExplainUrl = "https://explain.tensor.ru";
public static AppSettingsState getInstance() {
return ApplicationManager.getApplication().getService(AppSettingsState.class);
}
public void setExplainUrl(String explainUrl) {
ExplainUrl = explainUrl;
}
}
Роль контроллера - класс AppSettingsConfigurable реализующий интерфейс Configurable , в котором переопределены методы:
getDisplayName - выдает название пункта меню Settings, используется при поиске
public class AppSettingsConfigurable implements Configurable {
private AppSettingsComponent appSettingsComponent;
@Override
public @NlsContexts.ConfigurableName String getDisplayName() {
return "Explain PostgreSQL";
}
@Override
public @Nullable JComponent createComponent() {
appSettingsComponent = new AppSettingsComponent();
return appSettingsComponent.getPanel();
}
@Override
public void apply() throws ConfigurationException {
AppSettingsState settings = AppSettingsState.getInstance();
String explainUrl = appSettingsComponent.getExplainUrl();
try {
new URL(explainUrl).openStream().close();
settings.setExplainUrl(explainUrl);
} catch (Exception e) {
throw new ConfigurationException(e.getMessage());
}
}
}
apply - выполняет проверку на валидность URL и возможность подключиться к нему, в случае ошибок бросает исключение ConfigurationException , текст из которого выводится снизу на странице настроек.
createComponent - создает страницу настроек, о ней ниже.
Роль представления реализует класс AppSettingsComponent , в котором создается форма с текстовым полем и меткой:
public class AppSettingsComponent {
private final JPanel panel;
private final JBTextField explainUrlTextField = new JBTextField();
public AppSettingsComponent() {
panel = FormBuilder.createFormBuilder()
.addLabeledComponent(new JBLabel("Explain PostgreSQL site: "), explainUrlTextField, 1, false)
.addComponentFillVertically(new JPanel(), 0)
.getPanel();
}
public JPanel getPanel() {
return panel;
}
public String getExplainUrl() {
return explainUrlTextField.getText();
}
public void setExplainUrl(@NotNull String url) {
explainUrlTextField.setText(url);
}
}
Регистрируем модель и контроллер в plugin.xml , для контроллера необходимо указать пункт меню Settings, в котором будут наши настройки, т.е. tools:
<extensions defaultExtensionNs="com.intellij">
<applicationConfigurable parentId="tools"
instance="com.mgorkov.settings.AppSettingsConfigurable"
id="com.mgorkov.settings.AppSettingsConfigurable"
displayName="Explain PostgreSQL"
/>
<applicationService serviceImplementation="com.mgorkov.settings.AppSettingsState"/>
</extensions>
Отладка плагина
Для вывода сообщений в лог используем Logger :
private static final Logger log = Logger.getInstance(ExplainApiService.class);
log.debug("POST JSON: " + jsonObject);
Лог хранится в файле idea.log , открыть который можно через меню Help -> Open Log in Editor
По умолчанию в лог пишутся все события уровня INFO.
Чтобы записать события с уровнем DEBUG надо добавить список имен в Help -> Diagnostic Tools -> Debug Log Settings :
Для включения внутренних команд в IDE надо добавить опцию idea.is.internal=true в Help -> Edit Custom Properties
После перезапуска появится дополнительный пункт меню Tools -> Internal Actions
Теперь при одновременном нажатии Ctrl-Alt-A и щелчке мыши на любом элементе можно посмотреть его состав, как в DevTools браузера
Сборка и публикация плагина
Сборка осуществляется Gradle, при скачивании зависимостей в защищенной корпоративной среде могут быть ошибки типа "not found", для решения надо добавить сертификат в хранилище, используя стандартный пароль "changeit", и перезапустить IDE:
keytool -import -trustcacerts -alias root -file <файл с сертификатом> -keystore <путь к IDE>/jbr/lib/security/cacerts
Конфигурация сборки задается в файле build.gradle.kts , при необходимости можно поменять параметры по умолчанию. Например для варианта сборки с библиотекой json версии 20231013 и тестового запуска плагина в IDE IntelliJ IDEA Ultimate Edition версии 2022.2.5 :
dependencies {
implementation("org.json:json:20231013")
}
intellij {
version.set("2022.2.5")
type.set("IU") // Target IDE Platform
plugins.set(listOf("com.intellij.database"))
}
Описание всех опций сборки можно почитать здесь.
Gradle создает набор задач, для которых можно поменять конфигурацию запуска, например чтобы при тестовом запуске IDE лог-файл idea.log отображался в консоли в отдельной вкладке надо поменять конфигурацию задачи runIde и добавить idea.log , путь к которому можно посмотреть в Help -> Show Log in Files:
Для подписи плагина потребуется поменять конфигурацию задачи signPlugin и добавить несколько переменных окружения:
Эти переменные используются при сборке и уже прописаны в build.gradle.kts :
signPlugin {
certificateChain.set(System.getenv("CERTIFICATE_CHAIN"))
privateKey.set(System.getenv("PRIVATE_KEY"))
password.set(System.getenv("PRIVATE_KEY_PASSWORD"))
}
Процесс создания подписей есть в документации, но там нет описания для получения значений в виде одной строки, а это можно сделать так:
# строка для переменной PRIVATE_KEY
openssl genpkey -aes-256-cbc -algorithm RSA -pkeyopt rsa_keygen_bits:4096 | openssl rsa | tee private.pem | openssl base64 | tr -d '\n'
# строка для CERTIFICATE_CHAIN
openssl req -key private.pem -new -x509 -days 365 | openssl base64 | tr -d '\n'
Для проверки совместимости плагина с разными версиями версиями IDE можно использовать задачу verifyPlugin и добавить параметры для нее в конфиг сборки (build.gradle.kts) , например мы хотим проверить плагин в DataGrip версий 2023.3 и 3.1 :
runPluginVerifier {
ideVersions.set(listOf(
"DG-2023.3.1",
"DG-2023.3"
))
}
По умолчанию верификация плагина происходит на сайте plugins.jetbrains.com при загрузке новой версии плагина, на каких версиях производится проверка задается в build.gradle.kts :
patchPluginXml {
sinceBuild.set("222")
untilBuild.set("233.*")
}
Можно также запустить проверку на сайте для конкретной версии, например для версий 221.* плагин не совместим из-за отсутствия методов:
Для публикации плагина первичная загрузка производится вручную, собранный файл находится в build/distributions (его кстати можно установить прямо с диска, для этого зайти в Settings -> Plugins -> Install Plugin from Disk)
В дальнейшем можно автоматизировать публикацию, для этого на сайте plugins.jetbrains.com в своем профиле во вкладке Tokens сгенерить токен и прописать его в переменной PUBLISH_TOKEN в конфигурации задачи publishToken.
В статье написал обо всех этапах создания плагина, пропустив детали реализации, не относящиеся непосредственно к теме разработки плагинов в IDE JetBrains.
Код плагина опубликован в github под лицензией MIT.
Комментарии (17)
1nd1go
15.01.2024 13:36Спасибо за описание внутренностей плагина, как раз было интересно как можно самому сделать, а документация что-то у них не сильно наглядная
ris58h
15.01.2024 13:36Что именно показалось затруднительным в документации?
1nd1go
15.01.2024 13:36+1Ну она больше справочник, а хотелось бы "вот hello-world плагин, он устроен вот так, его можно расширять вот так", потому что я далек от написания таких компонентов, и по справочнику понять общую концепцию разработки сложно.
Apokalepsis
15.01.2024 13:36А что сервис делает сверх стандартных explain analyze?
Kilor
15.01.2024 13:36+1В базовой версии: корректно вычисляет потребление ресурсов на каждом узле с учетом вложенности, подсказывает, как можно устранить неэффективность в запросе, какие индексы стоит добавить в БД и наглядно это все показывает.
В расширенной версии добавляется возможность массового сквозного анализа всего лога сервера PostgreSQL, в том числе в онлайн-режиме.
otchgol
15.01.2024 13:36-2Всегда было интересно как находятся ресурсы на написание статей, но не находятся на поддержание плагина. Актуальная версия ide не принимает его.
MGorkov Автор
15.01.2024 13:36Какая у Вас ide и версия?
Obake
15.01.2024 13:36тоже ловлю ошибку на DataGrip 2023.3.2 (macOS):
java.lang.NoClassDefFoundError: org/json/JSONObject
at com.mgorkov.api.ExplainApiService.beautifier(ExplainApiService.java:47)MGorkov Автор
15.01.2024 13:36Проверяли на macOS Ventura 13.3.1 и DataGrip 2023.3.2 , ошибок нет.
На какой версии macOS запускаете?
Obake
15.01.2024 13:36M1 - macOS Sonoma Version 14.2.1 (23C71)
MGorkov Автор
15.01.2024 13:36На нашем M1 Sonoma все работает, https://youtu.be/_OH91vkcBZY
Возможно, конфликтует с другими плагинами. Можете прислать список установленных плагинов на почту me.gorkov@tensor.ru ?
boraldomaster
15.01.2024 13:36-1Я правильно понял, что для анализа запроса он отправляется на какой-то сторонний сервер, ещё и где-то в России? Тогда сразу масса вопросов.
Как можно сделать анализ запроса не имея под рукой схему базы данных?
Как можно сделать анализ запроса не имея под рукой самих данных (ведь использование индексов сильно зависит от количества строк в таблице, например иногда выгоднее делать Full Scan)?
Сливается ли это всё на российский сервер?
А если нет, точность таких анализов и сопутствующих "советов по оптимизации" вероятно мизерная.В идее есть стандартный встроенный плагин для построения плана запроса (он даже на ваших скриншотах виден). Насколько есть смысл рисковать словом данных непонятно куда вместо использования этого плагина?
Kilor
15.01.2024 13:36Давайте отвечу я, поскольку вопросы больше касаются сервиса, нежели плагина.
Если вас напрягает "сервер где-то в России", то можете настроить отправку из плагина на зеркало explain-postgresql.com "где-то в Amazon" или развернуть self-hosted версию где-то на собственных мощностях и отправлять туда.
Схема БД несложно восстанавливается на основании данных из плана, но она нужна только для принципиальных изменений алгоритма работы запроса, как вот тут, например.
Большинство проблем производительности запроса легко определяется на основании одного лишь плана, и решения там - типовые, в том числе и насчет индексов.
Shiftuia
Не знаю какое review JetBrains делает для плагинов, но я бы в любом случае по дефолту не доверял плагину, скачанному не из официального стора.
ris58h
Open source же. Бери да собирай.
Shiftuia
Согласен. Я и написал, что ПО, не собранное тобой, а скачанное из неизвестного источника, не заслуживает доверия.