2 * Copyright (c) 2010-2022 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.persistence.FilterCriteria;
31 import org.openhab.core.persistence.HistoricItem;
32 import org.openhab.core.persistence.PersistenceItemInfo;
33 import org.openhab.core.types.State;
34 import org.openhab.persistence.jdbc.internal.dto.Column;
35 import org.openhab.persistence.jdbc.internal.dto.ItemVO;
36 import org.openhab.persistence.jdbc.internal.dto.ItemsVO;
37 import org.openhab.persistence.jdbc.internal.dto.JdbcPersistenceItemInfo;
38 import org.openhab.persistence.jdbc.internal.exceptions.JdbcException;
39 import org.openhab.persistence.jdbc.internal.exceptions.JdbcSQLException;
40 import org.slf4j.Logger;
41 import org.slf4j.LoggerFactory;
43 import com.zaxxer.hikari.pool.HikariPool.PoolInitializationException;
48 * @author Helmut Lehmeyer - Initial contribution
51 public class JdbcMapper {
52 private final Logger logger = LoggerFactory.getLogger(JdbcMapper.class);
54 private final TimeZoneProvider timeZoneProvider;
56 // Error counter - used to reconnect to database on error
58 protected boolean initialized = false;
59 protected @NonNullByDefault({}) JdbcConfiguration conf;
60 protected final Map<String, String> itemNameToTableNameMap = new HashMap<>();
61 protected @NonNullByDefault({}) NamingStrategy namingStrategy;
62 private long afterAccessMin = 10000;
63 private long afterAccessMax = 0;
65 public JdbcMapper(TimeZoneProvider timeZoneProvider) {
66 this.timeZoneProvider = timeZoneProvider;
72 private boolean pingDB() throws JdbcSQLException {
73 logger.debug("JDBC::pingDB");
75 long timerStart = System.currentTimeMillis();
76 if (openConnection()) {
77 if (conf.getDbName() == null) {
79 "JDBC::pingDB asking db for name as absolutely first db action, after connection is established.");
80 String dbName = conf.getDBDAO().doGetDB();
84 conf.setDbName(dbName);
85 ret = dbName.length() > 0;
88 final @Nullable Integer result = conf.getDBDAO().doPingDB();
89 ret = result != null && result > 0;
92 logTime("pingDB", timerStart, System.currentTimeMillis());
96 private boolean ifItemsTableExists() throws JdbcSQLException {
97 logger.debug("JDBC::ifItemsTableExists");
98 long timerStart = System.currentTimeMillis();
99 ItemsVO vo = new ItemsVO();
100 vo.setItemsManageTable(conf.getItemsManageTable());
101 boolean res = conf.getDBDAO().doIfTableExists(vo);
102 logTime("doIfTableExists", timerStart, System.currentTimeMillis());
106 protected boolean ifTableExists(String tableName) throws JdbcSQLException {
107 logger.debug("JDBC::ifTableExists");
108 long timerStart = System.currentTimeMillis();
109 boolean res = conf.getDBDAO().doIfTableExists(tableName);
110 logTime("doIfTableExists", timerStart, System.currentTimeMillis());
114 private ItemsVO createNewEntryInItemsTable(ItemsVO vo) throws JdbcSQLException {
115 logger.debug("JDBC::createNewEntryInItemsTable");
116 long timerStart = System.currentTimeMillis();
117 Long i = conf.getDBDAO().doCreateNewEntryInItemsTable(vo);
118 vo.setItemId(i.intValue());
119 logTime("doCreateNewEntryInItemsTable", timerStart, System.currentTimeMillis());
123 private boolean createItemsTableIfNot(ItemsVO vo) throws JdbcSQLException {
124 logger.debug("JDBC::createItemsTableIfNot");
125 long timerStart = System.currentTimeMillis();
126 conf.getDBDAO().doCreateItemsTableIfNot(vo);
127 logTime("doCreateItemsTableIfNot", timerStart, System.currentTimeMillis());
131 private boolean dropItemsTableIfExists(ItemsVO vo) throws JdbcSQLException {
132 logger.debug("JDBC::dropItemsTableIfExists");
133 long timerStart = System.currentTimeMillis();
134 conf.getDBDAO().doDropItemsTableIfExists(vo);
135 logTime("doDropItemsTableIfExists", timerStart, System.currentTimeMillis());
139 protected void dropTable(String tableName) throws JdbcSQLException {
140 logger.debug("JDBC::dropTable");
141 long timerStart = System.currentTimeMillis();
142 conf.getDBDAO().doDropTable(tableName);
143 logTime("doDropTable", timerStart, System.currentTimeMillis());
146 protected ItemsVO deleteItemsEntry(ItemsVO vo) throws JdbcSQLException {
147 logger.debug("JDBC::deleteItemsEntry");
148 long timerStart = System.currentTimeMillis();
149 conf.getDBDAO().doDeleteItemsEntry(vo);
150 logTime("deleteItemsEntry", timerStart, System.currentTimeMillis());
154 private List<ItemsVO> getItemIDTableNames() throws JdbcSQLException {
155 logger.debug("JDBC::getItemIDTableNames");
156 long timerStart = System.currentTimeMillis();
157 ItemsVO isvo = new ItemsVO();
158 isvo.setItemsManageTable(conf.getItemsManageTable());
159 List<ItemsVO> vo = conf.getDBDAO().doGetItemIDTableNames(isvo);
160 logTime("getItemIDTableNames", timerStart, System.currentTimeMillis());
164 protected List<ItemsVO> getItemTables() throws JdbcSQLException {
165 logger.debug("JDBC::getItemTables");
166 long timerStart = System.currentTimeMillis();
167 ItemsVO isvo = new ItemsVO();
168 isvo.setJdbcUriDatabaseName(conf.getDbName());
169 isvo.setItemsManageTable(conf.getItemsManageTable());
170 List<ItemsVO> vol = conf.getDBDAO().doGetItemTables(isvo);
171 logTime("getItemTables", timerStart, System.currentTimeMillis());
175 protected List<Column> getTableColumns(String tableName) throws JdbcSQLException {
176 logger.debug("JDBC::getTableColumns");
177 long timerStart = System.currentTimeMillis();
178 ItemsVO isvo = new ItemsVO();
179 isvo.setJdbcUriDatabaseName(conf.getDbName());
180 isvo.setTableName(tableName);
181 List<Column> is = conf.getDBDAO().doGetTableColumns(isvo);
182 logTime("getTableColumns", timerStart, System.currentTimeMillis());
189 private void updateItemTableNames(List<ItemVO> vol) throws JdbcSQLException {
190 logger.debug("JDBC::updateItemTableNames");
191 long timerStart = System.currentTimeMillis();
192 conf.getDBDAO().doUpdateItemTableNames(vol);
193 logTime("updateItemTableNames", timerStart, System.currentTimeMillis());
196 private ItemVO createItemTable(ItemVO vo) throws JdbcSQLException {
197 logger.debug("JDBC::createItemTable");
198 long timerStart = System.currentTimeMillis();
199 conf.getDBDAO().doCreateItemTable(vo);
200 logTime("createItemTable", timerStart, System.currentTimeMillis());
204 protected void alterTableColumn(String tableName, String columnName, String columnType, boolean nullable)
205 throws JdbcSQLException {
206 logger.debug("JDBC::alterTableColumn");
207 long timerStart = System.currentTimeMillis();
208 conf.getDBDAO().doAlterTableColumn(tableName, columnName, columnType, nullable);
209 logTime("alterTableColumn", timerStart, System.currentTimeMillis());
212 protected void storeItemValue(Item item, State itemState, @Nullable ZonedDateTime date) throws JdbcException {
213 logger.debug("JDBC::storeItemValue: item={} state={} date={}", item, itemState, date);
214 String tableName = getTable(item);
215 long timerStart = System.currentTimeMillis();
217 conf.getDBDAO().doStoreItemValue(item, itemState, new ItemVO(tableName, null));
219 conf.getDBDAO().doStoreItemValue(item, itemState, new ItemVO(tableName, null), date);
221 logTime("storeItemValue", timerStart, System.currentTimeMillis());
225 public long getRowCount(String tableName) throws JdbcSQLException {
226 return conf.getDBDAO().doGetRowCount(tableName);
229 protected List<HistoricItem> getHistItemFilterQuery(FilterCriteria filter, int numberDecimalcount, String table,
230 Item item) throws JdbcSQLException {
232 "JDBC::getHistItemFilterQuery filter='{}' numberDecimalcount='{}' table='{}' item='{}' itemName='{}'",
233 true, numberDecimalcount, table, item, item.getName());
234 long timerStart = System.currentTimeMillis();
235 List<HistoricItem> result = conf.getDBDAO().doGetHistItemFilterQuery(item, filter, numberDecimalcount, table,
236 item.getName(), timeZoneProvider.getTimeZone());
237 logTime("getHistItemFilterQuery", timerStart, System.currentTimeMillis());
242 protected void deleteItemValues(FilterCriteria filter, String table) throws JdbcSQLException {
243 logger.debug("JDBC::deleteItemValues filter='{}' table='{}' itemName='{}'", true, table, filter.getItemName());
244 long timerStart = System.currentTimeMillis();
245 conf.getDBDAO().doDeleteItemValues(filter, table, timeZoneProvider.getTimeZone());
246 logTime("deleteItemValues", timerStart, System.currentTimeMillis());
250 /***********************
251 * DATABASE CONNECTION *
252 ***********************/
253 protected boolean openConnection() {
254 logger.debug("JDBC::openConnection isDriverAvailable: {}", conf.isDriverAvailable());
255 if (conf.isDriverAvailable() && !conf.isDbConnected()) {
256 logger.info("JDBC::openConnection: Driver is available::Yank setupDataSource");
258 Yank.setupDefaultConnectionPool(conf.getHikariConfiguration());
259 Yank.setThrowWrappedExceptions(true);
260 conf.setDbConnected(true);
262 } catch (PoolInitializationException e) {
263 Throwable cause = e.getCause();
264 if (cause instanceof SQLInvalidAuthorizationSpecException) {
265 logger.warn("JDBC::openConnection: failed to open connection: {}", cause.getMessage());
267 logger.warn("JDBC::openConnection: failed to open connection: {}", e.getMessage());
272 } else if (!conf.isDriverAvailable()) {
273 logger.warn("JDBC::openConnection: no driver available!");
280 protected void closeConnection() {
281 logger.debug("JDBC::closeConnection");
282 // Closes all open connection pools
283 Yank.releaseDefaultConnectionPool();
284 conf.setDbConnected(false);
287 protected boolean checkDBAccessability() {
288 // Check if connection is valid
294 boolean p = pingDB();
296 logger.debug("JDBC::checkDBAcessability, first try connection: {}", p);
297 return (p && !(conf.getErrReconnectThreshold() > 0 && errCnt <= conf.getErrReconnectThreshold()));
301 logger.debug("JDBC::checkDBAcessability, second try connection: {}", p);
302 return (p && !(conf.getErrReconnectThreshold() > 0 && errCnt <= conf.getErrReconnectThreshold()));
304 } catch (JdbcSQLException e) {
305 logger.warn("Unable to ping database", e);
310 /**************************
311 * DATABASE TABLEHANDLING *
312 **************************/
313 protected void checkDBSchema() throws JdbcSQLException {
314 ItemsVO vo = new ItemsVO();
315 vo.setItemsManageTable(conf.getItemsManageTable());
317 if (!conf.getTableUseRealCaseSensitiveItemNames()) {
318 createItemsTableIfNot(vo);
320 if (conf.getRebuildTableNames()) {
323 if (conf.getTableUseRealCaseSensitiveItemNames()) {
324 dropItemsTableIfExists(vo);
327 "JDBC::checkDBSchema: Rebuild complete, configure the 'rebuildTableNames' setting to 'false' to stop rebuilds on startup");
328 // Reset the error counter
331 populateItemNameToTableNameMap();
334 public void populateItemNameToTableNameMap() throws JdbcSQLException {
335 itemNameToTableNameMap.clear();
336 if (conf.getTableUseRealCaseSensitiveItemNames()) {
337 for (String itemName : getItemTables().stream().map(t -> t.getTableName()).collect(Collectors.toList())) {
338 itemNameToTableNameMap.put(itemName, itemName);
341 for (ItemsVO vo : getItemIDTableNames()) {
342 itemNameToTableNameMap.put(vo.getItemName(),
343 namingStrategy.getTableName(vo.getItemId(), vo.getItemName()));
348 protected String getTable(Item item) throws JdbcException {
349 String itemName = item.getName();
351 throw new JdbcException("Not initialized, unable to find table for item " + itemName);
354 // Table already exists - return the name
355 String tableName = itemNameToTableNameMap.get(itemName);
356 if (!Objects.isNull(tableName)) {
360 logger.debug("JDBC::getTable: no table found for item '{}' in itemNameToTableNameMap", itemName);
364 if (!conf.getTableUseRealCaseSensitiveItemNames()) {
365 // Create a new entry in items table
366 ItemsVO isvo = new ItemsVO();
367 isvo.setItemName(itemName);
368 isvo.setItemsManageTable(conf.getItemsManageTable());
369 isvo = createNewEntryInItemsTable(isvo);
370 itemId = isvo.getItemId();
372 logger.error("JDBC::getTable: Creating items entry for item '{}' failed.", itemName);
376 // Create the table name
377 logger.debug("JDBC::getTable: getTableName with rowId={} itemName={}", itemId, itemName);
378 tableName = namingStrategy.getTableName(itemId, itemName);
380 // Create table for item
381 String dataType = conf.getDBDAO().getDataType(item);
382 ItemVO ivo = new ItemVO(tableName, itemName);
383 ivo.setDbType(dataType);
384 ivo = createItemTable(ivo);
385 logger.debug("JDBC::getTable: Table created for item '{}' with dataType {} in SQL database.", itemName,
388 itemNameToTableNameMap.put(itemName, tableName);
393 private void formatTableNames() throws JdbcSQLException {
394 boolean tmpinit = initialized;
399 List<ItemsVO> itemIdTableNames = ifItemsTableExists() ? getItemIDTableNames() : new ArrayList<ItemsVO>();
400 var itemTables = getItemTables().stream().map(ItemsVO::getTableName).collect(Collectors.toList());
401 List<ItemVO> oldNewTableNames;
403 if (itemIdTableNames.isEmpty()) {
404 // Without mappings we can only migrate from direct item name to numeric mapping.
405 if (conf.getTableUseRealCaseSensitiveItemNames()) {
406 logger.info("JDBC::formatTableNames: Nothing to migrate.");
407 initialized = tmpinit;
410 oldNewTableNames = new ArrayList<>();
411 for (String itemName : itemTables) {
412 ItemsVO isvo = new ItemsVO();
413 isvo.setItemName(itemName);
414 isvo.setItemsManageTable(conf.getItemsManageTable());
415 isvo = createNewEntryInItemsTable(isvo);
416 int itemId = isvo.getItemId();
418 logger.error("JDBC::formatTableNames: Creating items entry for item '{}' failed.", itemName);
420 String newTableName = namingStrategy.getTableName(itemId, itemName);
421 oldNewTableNames.add(new ItemVO(itemName, newTableName));
422 logger.info("JDBC::formatTableNames: Table '{}' will be renamed to '{}'", itemName, newTableName);
426 String itemsManageTable = conf.getItemsManageTable();
427 Map<Integer, String> itemIdToItemNameMap = new HashMap<>();
429 for (ItemsVO vo : itemIdTableNames) {
430 int itemId = vo.getItemId();
431 String itemName = vo.getItemName();
432 itemIdToItemNameMap.put(itemId, itemName);
435 oldNewTableNames = namingStrategy.prepareMigration(itemTables, itemIdToItemNameMap, itemsManageTable);
438 updateItemTableNames(oldNewTableNames);
439 logger.info("JDBC::formatTableNames: Finished updating {} item table names", oldNewTableNames.size());
441 initialized = tmpinit;
444 protected Set<PersistenceItemInfo> getItems() {
445 // TODO: in general it would be possible to query the count, earliest and latest values for each item too but it
446 // would be a very costly operation
447 return itemNameToTableNameMap.keySet().stream().map(itemName -> new JdbcPersistenceItemInfo(itemName))
448 .collect(Collectors.<PersistenceItemInfo> toSet());
454 private void logTime(String me, long timerStart, long timerStop) {
455 if (conf.enableLogTime && logger.isInfoEnabled()) {
457 int timerDiff = (int) (timerStop - timerStart);
458 if (timerDiff < afterAccessMin) {
459 afterAccessMin = timerDiff;
461 if (timerDiff > afterAccessMax) {
462 afterAccessMax = timerDiff;
464 conf.timeAverage50arr.add(timerDiff);
465 conf.timeAverage100arr.add(timerDiff);
466 conf.timeAverage200arr.add(timerDiff);
467 if (conf.timerCount == 1) {
468 conf.timer1000 = System.currentTimeMillis();
470 if (conf.timerCount == 1001) {
471 conf.time1000Statements = Math.round(((int) (System.currentTimeMillis() - conf.timer1000)) / 1000);// Seconds
475 "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",
476 me, timerDiff, conf.timeAverage50arr.getAverageInteger(),
477 conf.timeAverage100arr.getAverageInteger(), conf.timeAverage200arr.getAverageInteger(),
478 afterAccessMin, afterAccessMax, conf.time1000Statements, conf.timerCount);