2 * Copyright (c) 2010-2023 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
7 * This program and the accompanying materials are made available under the
8 * terms of the Eclipse Public License 2.0 which is available at
9 * http://www.eclipse.org/legal/epl-2.0
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.persistence.jdbc.internal;
15 import java.sql.SQLInvalidAuthorizationSpecException;
16 import java.time.ZonedDateTime;
17 import java.util.ArrayList;
18 import java.util.HashMap;
19 import java.util.List;
21 import java.util.Objects;
23 import java.util.stream.Collectors;
25 import org.eclipse.jdt.annotation.NonNullByDefault;
26 import org.eclipse.jdt.annotation.Nullable;
27 import org.knowm.yank.Yank;
28 import org.openhab.core.i18n.TimeZoneProvider;
29 import org.openhab.core.items.Item;
30 import org.openhab.core.items.ItemUtil;
31 import org.openhab.core.persistence.FilterCriteria;
32 import org.openhab.core.persistence.HistoricItem;
33 import org.openhab.core.persistence.PersistenceItemInfo;
34 import org.openhab.core.types.State;
35 import org.openhab.persistence.jdbc.internal.dto.Column;
36 import org.openhab.persistence.jdbc.internal.dto.ItemVO;
37 import org.openhab.persistence.jdbc.internal.dto.ItemsVO;
38 import org.openhab.persistence.jdbc.internal.dto.JdbcPersistenceItemInfo;
39 import org.openhab.persistence.jdbc.internal.exceptions.JdbcException;
40 import org.openhab.persistence.jdbc.internal.exceptions.JdbcSQLException;
41 import org.slf4j.Logger;
42 import org.slf4j.LoggerFactory;
44 import com.zaxxer.hikari.pool.HikariPool.PoolInitializationException;
49 * @author Helmut Lehmeyer - Initial contribution
52 public class JdbcMapper {
53 private static final int MIGRATION_PERCENTAGE_THRESHOLD = 50;
55 private final Logger logger = LoggerFactory.getLogger(JdbcMapper.class);
56 private final TimeZoneProvider timeZoneProvider;
58 // Error counter - used to reconnect to database on error
60 protected boolean initialized = false;
61 protected @NonNullByDefault({}) JdbcConfiguration conf;
62 protected final Map<String, String> itemNameToTableNameMap = new HashMap<>();
63 protected @NonNullByDefault({}) NamingStrategy namingStrategy;
64 private long afterAccessMin = 10000;
65 private long afterAccessMax = 0;
67 public JdbcMapper(TimeZoneProvider timeZoneProvider) {
68 this.timeZoneProvider = timeZoneProvider;
74 private boolean pingDB() throws JdbcSQLException {
75 logger.debug("JDBC::pingDB");
77 long timerStart = System.currentTimeMillis();
78 if (openConnection()) {
79 if (conf.getDbName() == null) {
81 "JDBC::pingDB asking db for name as absolutely first db action, after connection is established.");
82 String dbName = conf.getDBDAO().doGetDB();
86 conf.setDbName(dbName);
87 ret = dbName.length() > 0;
90 final @Nullable Integer result = conf.getDBDAO().doPingDB();
91 ret = result != null && result > 0;
94 logTime("pingDB", timerStart, System.currentTimeMillis());
98 private boolean ifItemsTableExists() throws JdbcSQLException {
99 logger.debug("JDBC::ifItemsTableExists");
100 long timerStart = System.currentTimeMillis();
101 ItemsVO vo = new ItemsVO();
102 vo.setItemsManageTable(conf.getItemsManageTable());
103 boolean res = conf.getDBDAO().doIfTableExists(vo);
104 logTime("doIfTableExists", timerStart, System.currentTimeMillis());
108 protected boolean ifTableExists(String tableName) throws JdbcSQLException {
109 logger.debug("JDBC::ifTableExists");
110 long timerStart = System.currentTimeMillis();
111 boolean res = conf.getDBDAO().doIfTableExists(tableName);
112 logTime("doIfTableExists", timerStart, System.currentTimeMillis());
116 private ItemsVO createNewEntryInItemsTable(ItemsVO vo) throws JdbcSQLException {
117 logger.debug("JDBC::createNewEntryInItemsTable");
118 long timerStart = System.currentTimeMillis();
119 Long i = conf.getDBDAO().doCreateNewEntryInItemsTable(vo);
120 vo.setItemId(i.intValue());
121 logTime("doCreateNewEntryInItemsTable", timerStart, System.currentTimeMillis());
125 private boolean createItemsTableIfNot(ItemsVO vo) throws JdbcSQLException {
126 logger.debug("JDBC::createItemsTableIfNot");
127 long timerStart = System.currentTimeMillis();
128 conf.getDBDAO().doCreateItemsTableIfNot(vo);
129 logTime("doCreateItemsTableIfNot", timerStart, System.currentTimeMillis());
133 private boolean dropItemsTableIfExists(ItemsVO vo) throws JdbcSQLException {
134 logger.debug("JDBC::dropItemsTableIfExists");
135 long timerStart = System.currentTimeMillis();
136 conf.getDBDAO().doDropItemsTableIfExists(vo);
137 logTime("doDropItemsTableIfExists", timerStart, System.currentTimeMillis());
141 protected void dropTable(String tableName) throws JdbcSQLException {
142 logger.debug("JDBC::dropTable");
143 long timerStart = System.currentTimeMillis();
144 conf.getDBDAO().doDropTable(tableName);
145 logTime("doDropTable", timerStart, System.currentTimeMillis());
148 protected ItemsVO deleteItemsEntry(ItemsVO vo) throws JdbcSQLException {
149 logger.debug("JDBC::deleteItemsEntry");
150 long timerStart = System.currentTimeMillis();
151 conf.getDBDAO().doDeleteItemsEntry(vo);
152 logTime("deleteItemsEntry", timerStart, System.currentTimeMillis());
156 private List<ItemsVO> getItemIDTableNames() throws JdbcSQLException {
157 logger.debug("JDBC::getItemIDTableNames");
158 long timerStart = System.currentTimeMillis();
159 ItemsVO isvo = new ItemsVO();
160 isvo.setItemsManageTable(conf.getItemsManageTable());
161 List<ItemsVO> vo = conf.getDBDAO().doGetItemIDTableNames(isvo);
162 logTime("getItemIDTableNames", timerStart, System.currentTimeMillis());
166 protected List<ItemsVO> getItemTables() throws JdbcSQLException {
167 logger.debug("JDBC::getItemTables");
168 long timerStart = System.currentTimeMillis();
169 ItemsVO isvo = new ItemsVO();
170 isvo.setJdbcUriDatabaseName(conf.getDbName());
171 isvo.setItemsManageTable(conf.getItemsManageTable());
172 List<ItemsVO> vol = conf.getDBDAO().doGetItemTables(isvo);
173 logTime("getItemTables", timerStart, System.currentTimeMillis());
177 protected List<Column> getTableColumns(String tableName) throws JdbcSQLException {
178 logger.debug("JDBC::getTableColumns");
179 long timerStart = System.currentTimeMillis();
180 ItemsVO isvo = new ItemsVO();
181 isvo.setJdbcUriDatabaseName(conf.getDbName());
182 isvo.setTableName(tableName);
183 isvo.setItemsManageTable(conf.getItemsManageTable());
184 List<Column> is = conf.getDBDAO().doGetTableColumns(isvo);
185 logTime("getTableColumns", timerStart, System.currentTimeMillis());
192 private void updateItemTableNames(List<ItemVO> vol) throws JdbcSQLException {
193 logger.debug("JDBC::updateItemTableNames");
194 long timerStart = System.currentTimeMillis();
195 conf.getDBDAO().doUpdateItemTableNames(vol);
196 logTime("updateItemTableNames", timerStart, System.currentTimeMillis());
199 private ItemVO createItemTable(ItemVO vo) throws JdbcSQLException {
200 logger.debug("JDBC::createItemTable");
201 long timerStart = System.currentTimeMillis();
202 conf.getDBDAO().doCreateItemTable(vo);
203 logTime("createItemTable", timerStart, System.currentTimeMillis());
207 protected void alterTableColumn(String tableName, String columnName, String columnType, boolean nullable)
208 throws JdbcSQLException {
209 logger.debug("JDBC::alterTableColumn");
210 long timerStart = System.currentTimeMillis();
211 conf.getDBDAO().doAlterTableColumn(tableName, columnName, columnType, nullable);
212 logTime("alterTableColumn", timerStart, System.currentTimeMillis());
215 protected void storeItemValue(Item item, State itemState, @Nullable ZonedDateTime date) throws JdbcException {
216 logger.debug("JDBC::storeItemValue: item={} state={} date={}", item, itemState, date);
217 String tableName = getTable(item);
218 long timerStart = System.currentTimeMillis();
220 conf.getDBDAO().doStoreItemValue(item, itemState, new ItemVO(tableName, null));
222 conf.getDBDAO().doStoreItemValue(item, itemState, new ItemVO(tableName, null), date);
224 logTime("storeItemValue", timerStart, System.currentTimeMillis());
228 public long getRowCount(String tableName) throws JdbcSQLException {
229 return conf.getDBDAO().doGetRowCount(tableName);
232 protected List<HistoricItem> getHistItemFilterQuery(FilterCriteria filter, int numberDecimalcount, String table,
233 Item item) throws JdbcSQLException {
235 "JDBC::getHistItemFilterQuery filter='{}' numberDecimalcount='{}' table='{}' item='{}' itemName='{}'",
236 true, numberDecimalcount, table, item, item.getName());
237 long timerStart = System.currentTimeMillis();
238 List<HistoricItem> result = conf.getDBDAO().doGetHistItemFilterQuery(item, filter, numberDecimalcount, table,
239 item.getName(), timeZoneProvider.getTimeZone());
240 logTime("getHistItemFilterQuery", timerStart, System.currentTimeMillis());
245 protected void deleteItemValues(FilterCriteria filter, String table) throws JdbcSQLException {
246 logger.debug("JDBC::deleteItemValues filter='{}' table='{}' itemName='{}'", true, table, filter.getItemName());
247 long timerStart = System.currentTimeMillis();
248 conf.getDBDAO().doDeleteItemValues(filter, table, timeZoneProvider.getTimeZone());
249 logTime("deleteItemValues", timerStart, System.currentTimeMillis());
253 /***********************
254 * DATABASE CONNECTION *
255 ***********************/
256 protected boolean openConnection() {
257 logger.debug("JDBC::openConnection isDriverAvailable: {}", conf.isDriverAvailable());
258 if (conf.isDriverAvailable() && !conf.isDbConnected()) {
259 logger.info("JDBC::openConnection: Driver is available::Yank setupDataSource");
261 Yank.setupDefaultConnectionPool(conf.getHikariConfiguration());
262 Yank.setThrowWrappedExceptions(true);
263 conf.setDbConnected(true);
265 } catch (PoolInitializationException e) {
266 Throwable cause = e.getCause();
267 if (cause instanceof SQLInvalidAuthorizationSpecException) {
268 logger.warn("JDBC::openConnection: failed to open connection: {}", cause.getMessage());
270 logger.warn("JDBC::openConnection: failed to open connection: {}", e.getMessage());
275 } else if (!conf.isDriverAvailable()) {
276 logger.warn("JDBC::openConnection: no driver available!");
283 protected void closeConnection() {
284 logger.debug("JDBC::closeConnection");
285 // Closes all open connection pools
286 Yank.releaseDefaultConnectionPool();
287 conf.setDbConnected(false);
290 protected boolean checkDBAccessability() {
291 // Check if connection is valid
297 boolean p = pingDB();
299 logger.debug("JDBC::checkDBAcessability, first try connection: {}", p);
300 return (p && !(conf.getErrReconnectThreshold() > 0 && errCnt <= conf.getErrReconnectThreshold()));
304 logger.debug("JDBC::checkDBAcessability, second try connection: {}", p);
305 return (p && !(conf.getErrReconnectThreshold() > 0 && errCnt <= conf.getErrReconnectThreshold()));
307 } catch (JdbcSQLException e) {
308 logger.warn("Unable to ping database", e);
313 /**************************
314 * DATABASE TABLEHANDLING *
315 **************************/
316 protected void checkDBSchema() throws JdbcSQLException {
317 ItemsVO vo = new ItemsVO();
318 vo.setItemsManageTable(conf.getItemsManageTable());
320 if (!conf.getTableUseRealCaseSensitiveItemNames()) {
321 createItemsTableIfNot(vo);
323 if (conf.getRebuildTableNames()) {
326 if (conf.getTableUseRealCaseSensitiveItemNames()) {
327 dropItemsTableIfExists(vo);
330 "JDBC::checkDBSchema: Rebuild complete, configure the 'rebuildTableNames' setting to 'false' to stop rebuilds on startup");
331 // Reset the error counter
334 populateItemNameToTableNameMap();
337 public void populateItemNameToTableNameMap() throws JdbcSQLException {
338 itemNameToTableNameMap.clear();
339 if (conf.getTableUseRealCaseSensitiveItemNames()) {
340 for (String itemName : getItemTables().stream().map(t -> t.getTableName()).collect(Collectors.toList())) {
341 itemNameToTableNameMap.put(itemName, itemName);
344 for (ItemsVO vo : getItemIDTableNames()) {
345 String itemName = vo.getItemName();
346 if (!ItemUtil.isValidItemName(itemName)) {
347 logger.warn("Skipping invalid item name {}", itemName);
350 itemNameToTableNameMap.put(vo.getItemName(),
351 namingStrategy.getTableName(vo.getItemId(), vo.getItemName()));
356 protected String getTable(Item item) throws JdbcException {
357 String itemName = item.getName();
359 throw new JdbcException("Not initialized, unable to find table for item " + itemName);
362 // Table already exists - return the name
363 String tableName = itemNameToTableNameMap.get(itemName);
364 if (!Objects.isNull(tableName)) {
368 logger.debug("JDBC::getTable: no table found for item '{}' in itemNameToTableNameMap", itemName);
372 if (!conf.getTableUseRealCaseSensitiveItemNames()) {
373 // Create a new entry in items table
374 ItemsVO isvo = new ItemsVO();
375 isvo.setItemName(itemName);
376 isvo.setItemsManageTable(conf.getItemsManageTable());
377 isvo = createNewEntryInItemsTable(isvo);
378 itemId = isvo.getItemId();
380 logger.error("JDBC::getTable: Creating items entry for item '{}' failed.", itemName);
384 // Create the table name
385 logger.debug("JDBC::getTable: getTableName with rowId={} itemName={}", itemId, itemName);
386 tableName = namingStrategy.getTableName(itemId, itemName);
388 // Create table for item
389 String dataType = conf.getDBDAO().getDataType(item);
390 ItemVO ivo = new ItemVO(tableName, itemName);
391 ivo.setDbType(dataType);
392 ivo = createItemTable(ivo);
393 logger.debug("JDBC::getTable: Table created for item '{}' with dataType {} in SQL database.", itemName,
396 itemNameToTableNameMap.put(itemName, tableName);
401 private void formatTableNames() throws JdbcSQLException {
402 boolean tmpinit = initialized;
407 List<ItemsVO> itemIdTableNames = ifItemsTableExists() ? getItemIDTableNames() : new ArrayList<ItemsVO>();
408 var itemTables = getItemTables().stream().map(ItemsVO::getTableName).collect(Collectors.toList());
409 List<ItemVO> oldNewTableNames;
411 if (itemIdTableNames.isEmpty()) {
412 // Without mappings we can only migrate from direct item name to numeric mapping.
413 if (conf.getTableUseRealCaseSensitiveItemNames()) {
414 logger.info("JDBC::formatTableNames: Nothing to migrate.");
415 initialized = tmpinit;
418 // Safety valve to prevent accidental migrations.
419 int numberOfTables = itemTables.size();
420 if (numberOfTables > 0) {
421 String prefix = conf.getTableNamePrefix();
422 long numberOfItemsWithPrefix = itemTables.stream()
423 .filter(i -> i.startsWith(prefix) || i.toLowerCase().startsWith("item")).count();
424 long percentageWithPrefix = (numberOfItemsWithPrefix * 100) / itemTables.size();
425 if (!prefix.isBlank() && percentageWithPrefix >= MIGRATION_PERCENTAGE_THRESHOLD) {
427 "JDBC::formatTableNames: {}% of all tables start with table name prefix '{}' or 'item', but items manage table '{}' was not found or is empty. Check configuration parameter 'itemsManageTable'",
428 percentageWithPrefix, conf.getTableNamePrefix(), conf.getItemsManageTable());
429 if (ifTableExists("items")) {
431 "JDBC::formatTableNames: Table 'items' was found, consider updating configuration parameter 'itemsManageTable' accordingly");
433 initialized = tmpinit;
437 oldNewTableNames = new ArrayList<>();
438 for (String itemName : itemTables) {
439 if (!ItemUtil.isValidItemName(itemName)) {
440 logger.warn("JDBC::formatTableNames: Skipping invalid item name {}", itemName);
443 ItemsVO isvo = new ItemsVO();
444 isvo.setItemName(itemName);
445 isvo.setItemsManageTable(conf.getItemsManageTable());
446 isvo = createNewEntryInItemsTable(isvo);
447 int itemId = isvo.getItemId();
449 logger.error("JDBC::formatTableNames: Creating items entry for item '{}' failed.", itemName);
451 String newTableName = namingStrategy.getTableName(itemId, itemName);
452 oldNewTableNames.add(new ItemVO(itemName, newTableName));
453 logger.info("JDBC::formatTableNames: Table '{}' will be renamed to '{}'", itemName, newTableName);
457 String itemsManageTable = conf.getItemsManageTable();
458 Map<Integer, String> itemIdToItemNameMap = new HashMap<>();
460 for (ItemsVO vo : itemIdTableNames) {
461 int itemId = vo.getItemId();
462 String itemName = vo.getItemName();
463 itemIdToItemNameMap.put(itemId, itemName);
466 oldNewTableNames = namingStrategy.prepareMigration(itemTables, itemIdToItemNameMap, itemsManageTable);
469 updateItemTableNames(oldNewTableNames);
470 logger.info("JDBC::formatTableNames: Finished updating {} item table names", oldNewTableNames.size());
472 initialized = tmpinit;
475 protected Set<PersistenceItemInfo> getItems() {
476 // TODO: in general it would be possible to query the count, earliest and latest values for each item too but it
477 // would be a very costly operation
478 return itemNameToTableNameMap.keySet().stream().map(itemName -> new JdbcPersistenceItemInfo(itemName))
479 .collect(Collectors.<PersistenceItemInfo> toSet());
485 private void logTime(String me, long timerStart, long timerStop) {
486 if (conf.enableLogTime && logger.isInfoEnabled()) {
488 int timerDiff = (int) (timerStop - timerStart);
489 if (timerDiff < afterAccessMin) {
490 afterAccessMin = timerDiff;
492 if (timerDiff > afterAccessMax) {
493 afterAccessMax = timerDiff;
495 conf.timeAverage50arr.add(timerDiff);
496 conf.timeAverage100arr.add(timerDiff);
497 conf.timeAverage200arr.add(timerDiff);
498 if (conf.timerCount == 1) {
499 conf.timer1000 = System.currentTimeMillis();
501 if (conf.timerCount == 1001) {
502 conf.time1000Statements = Math.round(((int) (System.currentTimeMillis() - conf.timer1000)) / 1000);// Seconds
506 "JDBC::logTime: '{}':\n afterAccess = {} ms\n timeAverage50 = {} ms\n timeAverage100 = {} ms\n timeAverage200 = {} ms\n afterAccessMin = {} ms\n afterAccessMax = {} ms\n 1000Statements = {} sec\n statementCount = {}\n",
507 me, timerDiff, conf.timeAverage50arr.getAverageInteger(),
508 conf.timeAverage100arr.getAverageInteger(), conf.timeAverage200arr.getAverageInteger(),
509 afterAccessMin, afterAccessMax, conf.time1000Statements, conf.timerCount);