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.ItemVO;
35 import org.openhab.persistence.jdbc.internal.dto.ItemsVO;
36 import org.openhab.persistence.jdbc.internal.dto.JdbcPersistenceItemInfo;
37 import org.openhab.persistence.jdbc.internal.exceptions.JdbcSQLException;
38 import org.slf4j.Logger;
39 import org.slf4j.LoggerFactory;
41 import com.zaxxer.hikari.pool.HikariPool.PoolInitializationException;
46 * @author Helmut Lehmeyer - Initial contribution
49 public class JdbcMapper {
50 private final Logger logger = LoggerFactory.getLogger(JdbcMapper.class);
52 private final TimeZoneProvider timeZoneProvider;
54 // Error counter - used to reconnect to database on error
56 protected boolean initialized = false;
57 protected @NonNullByDefault({}) JdbcConfiguration conf;
58 protected final Map<String, String> itemNameToTableNameMap = new HashMap<>();
59 protected @NonNullByDefault({}) NamingStrategy namingStrategy;
60 private long afterAccessMin = 10000;
61 private long afterAccessMax = 0;
63 public JdbcMapper(TimeZoneProvider timeZoneProvider) {
64 this.timeZoneProvider = timeZoneProvider;
70 private boolean pingDB() throws JdbcSQLException {
71 logger.debug("JDBC::pingDB");
73 long timerStart = System.currentTimeMillis();
74 if (openConnection()) {
75 if (conf.getDbName() == null) {
77 "JDBC::pingDB asking db for name as absolutely first db action, after connection is established.");
78 String dbName = conf.getDBDAO().doGetDB();
82 conf.setDbName(dbName);
83 ret = dbName.length() > 0;
86 final @Nullable Integer result = conf.getDBDAO().doPingDB();
87 ret = result != null && result > 0;
90 logTime("pingDB", timerStart, System.currentTimeMillis());
94 private boolean ifItemsTableExists() throws JdbcSQLException {
95 logger.debug("JDBC::ifItemsTableExists");
96 long timerStart = System.currentTimeMillis();
97 boolean res = conf.getDBDAO().doIfTableExists(new ItemsVO());
98 logTime("doIfTableExists", timerStart, System.currentTimeMillis());
102 protected boolean ifTableExists(String tableName) throws JdbcSQLException {
103 logger.debug("JDBC::ifTableExists");
104 long timerStart = System.currentTimeMillis();
105 boolean res = conf.getDBDAO().doIfTableExists(tableName);
106 logTime("doIfTableExists", timerStart, System.currentTimeMillis());
110 private ItemsVO createNewEntryInItemsTable(ItemsVO vo) throws JdbcSQLException {
111 logger.debug("JDBC::createNewEntryInItemsTable");
112 long timerStart = System.currentTimeMillis();
113 Long i = conf.getDBDAO().doCreateNewEntryInItemsTable(vo);
114 vo.setItemId(i.intValue());
115 logTime("doCreateNewEntryInItemsTable", timerStart, System.currentTimeMillis());
119 private boolean createItemsTableIfNot(ItemsVO vo) throws JdbcSQLException {
120 logger.debug("JDBC::createItemsTableIfNot");
121 long timerStart = System.currentTimeMillis();
122 conf.getDBDAO().doCreateItemsTableIfNot(vo);
123 logTime("doCreateItemsTableIfNot", timerStart, System.currentTimeMillis());
127 private boolean dropItemsTableIfExists(ItemsVO vo) throws JdbcSQLException {
128 logger.debug("JDBC::dropItemsTableIfExists");
129 long timerStart = System.currentTimeMillis();
130 conf.getDBDAO().doDropItemsTableIfExists(vo);
131 logTime("doDropItemsTableIfExists", timerStart, System.currentTimeMillis());
135 protected void dropTable(String tableName) throws JdbcSQLException {
136 logger.debug("JDBC::dropTable");
137 long timerStart = System.currentTimeMillis();
138 conf.getDBDAO().doDropTable(tableName);
139 logTime("doDropTable", timerStart, System.currentTimeMillis());
142 protected ItemsVO deleteItemsEntry(ItemsVO vo) throws JdbcSQLException {
143 logger.debug("JDBC::deleteItemsEntry");
144 long timerStart = System.currentTimeMillis();
145 conf.getDBDAO().doDeleteItemsEntry(vo);
146 logTime("deleteItemsEntry", timerStart, System.currentTimeMillis());
150 private List<ItemsVO> getItemIDTableNames() throws JdbcSQLException {
151 logger.debug("JDBC::getItemIDTableNames");
152 long timerStart = System.currentTimeMillis();
153 List<ItemsVO> vo = conf.getDBDAO().doGetItemIDTableNames(new ItemsVO());
154 logTime("getItemIDTableNames", timerStart, System.currentTimeMillis());
158 protected List<ItemsVO> getItemTables() throws JdbcSQLException {
159 logger.debug("JDBC::getItemTables");
160 long timerStart = System.currentTimeMillis();
161 ItemsVO vo = new ItemsVO();
162 vo.setJdbcUriDatabaseName(conf.getDbName());
163 List<ItemsVO> vol = conf.getDBDAO().doGetItemTables(vo);
164 logTime("getItemTables", timerStart, System.currentTimeMillis());
171 private void updateItemTableNames(List<ItemVO> vol) throws JdbcSQLException {
172 logger.debug("JDBC::updateItemTableNames");
173 long timerStart = System.currentTimeMillis();
174 conf.getDBDAO().doUpdateItemTableNames(vol);
175 logTime("updateItemTableNames", timerStart, System.currentTimeMillis());
178 private ItemVO createItemTable(ItemVO vo) throws JdbcSQLException {
179 logger.debug("JDBC::createItemTable");
180 long timerStart = System.currentTimeMillis();
181 conf.getDBDAO().doCreateItemTable(vo);
182 logTime("createItemTable", timerStart, System.currentTimeMillis());
186 protected void storeItemValue(Item item, State itemState, @Nullable ZonedDateTime date) throws JdbcSQLException {
187 logger.debug("JDBC::storeItemValue: item={} state={} date={}", item, itemState, date);
188 String tableName = getTable(item);
189 long timerStart = System.currentTimeMillis();
191 conf.getDBDAO().doStoreItemValue(item, itemState, new ItemVO(tableName, null));
193 conf.getDBDAO().doStoreItemValue(item, itemState, new ItemVO(tableName, null), date);
195 logTime("storeItemValue", timerStart, System.currentTimeMillis());
199 public long getRowCount(String tableName) throws JdbcSQLException {
200 return conf.getDBDAO().doGetRowCount(tableName);
203 protected List<HistoricItem> getHistItemFilterQuery(FilterCriteria filter, int numberDecimalcount, String table,
204 Item item) throws JdbcSQLException {
206 "JDBC::getHistItemFilterQuery filter='{}' numberDecimalcount='{}' table='{}' item='{}' itemName='{}'",
207 true, numberDecimalcount, table, item, item.getName());
208 long timerStart = System.currentTimeMillis();
209 List<HistoricItem> result = conf.getDBDAO().doGetHistItemFilterQuery(item, filter, numberDecimalcount, table,
210 item.getName(), timeZoneProvider.getTimeZone());
211 logTime("getHistItemFilterQuery", timerStart, System.currentTimeMillis());
216 protected void deleteItemValues(FilterCriteria filter, String table) throws JdbcSQLException {
217 logger.debug("JDBC::deleteItemValues filter='{}' table='{}' itemName='{}'", true, table, filter.getItemName());
218 long timerStart = System.currentTimeMillis();
219 conf.getDBDAO().doDeleteItemValues(filter, table, timeZoneProvider.getTimeZone());
220 logTime("deleteItemValues", timerStart, System.currentTimeMillis());
224 /***********************
225 * DATABASE CONNECTION *
226 ***********************/
227 protected boolean openConnection() {
228 logger.debug("JDBC::openConnection isDriverAvailable: {}", conf.isDriverAvailable());
229 if (conf.isDriverAvailable() && !conf.isDbConnected()) {
230 logger.info("JDBC::openConnection: Driver is available::Yank setupDataSource");
232 Yank.setupDefaultConnectionPool(conf.getHikariConfiguration());
233 Yank.setThrowWrappedExceptions(true);
234 conf.setDbConnected(true);
236 } catch (PoolInitializationException e) {
237 Throwable cause = e.getCause();
238 if (cause instanceof SQLInvalidAuthorizationSpecException) {
239 logger.warn("JDBC::openConnection: failed to open connection: {}", cause.getMessage());
241 logger.warn("JDBC::openConnection: failed to open connection: {}", e.getMessage());
246 } else if (!conf.isDriverAvailable()) {
247 logger.warn("JDBC::openConnection: no driver available!");
254 protected void closeConnection() {
255 logger.debug("JDBC::closeConnection");
256 // Closes all open connection pools
257 Yank.releaseDefaultConnectionPool();
258 conf.setDbConnected(false);
261 protected boolean checkDBAccessability() {
262 // Check if connection is valid
268 boolean p = pingDB();
270 logger.debug("JDBC::checkDBAcessability, first try connection: {}", p);
271 return (p && !(conf.getErrReconnectThreshold() > 0 && errCnt <= conf.getErrReconnectThreshold()));
275 logger.debug("JDBC::checkDBAcessability, second try connection: {}", p);
276 return (p && !(conf.getErrReconnectThreshold() > 0 && errCnt <= conf.getErrReconnectThreshold()));
278 } catch (JdbcSQLException e) {
279 logger.warn("Unable to ping database", e);
284 /**************************
285 * DATABASE TABLEHANDLING *
286 **************************/
287 protected void checkDBSchema() throws JdbcSQLException {
288 if (!conf.getTableUseRealCaseSensitiveItemNames()) {
289 createItemsTableIfNot(new ItemsVO());
291 if (conf.getRebuildTableNames()) {
294 if (conf.getTableUseRealCaseSensitiveItemNames()) {
295 dropItemsTableIfExists(new ItemsVO());
298 "JDBC::checkDBSchema: Rebuild complete, configure the 'rebuildTableNames' setting to 'false' to stop rebuilds on startup");
299 // Reset the error counter
302 populateItemNameToTableNameMap();
305 public void populateItemNameToTableNameMap() throws JdbcSQLException {
306 itemNameToTableNameMap.clear();
307 if (conf.getTableUseRealCaseSensitiveItemNames()) {
308 for (String itemName : getItemTables().stream().map(t -> t.getTableName()).collect(Collectors.toList())) {
309 itemNameToTableNameMap.put(itemName, itemName);
312 for (ItemsVO vo : getItemIDTableNames()) {
313 itemNameToTableNameMap.put(vo.getItemName(),
314 namingStrategy.getTableName(vo.getItemId(), vo.getItemName()));
319 protected String getTable(Item item) throws JdbcSQLException {
324 String itemName = item.getName();
325 String tableName = itemNameToTableNameMap.get(itemName);
327 // Table already exists - return the name
328 if (!Objects.isNull(tableName)) {
332 logger.debug("JDBC::getTable: no table found for item '{}' in sqlTables", itemName);
334 if (!conf.getTableUseRealCaseSensitiveItemNames()) {
335 // Create a new entry in items table
336 isvo = new ItemsVO();
337 isvo.setItemName(itemName);
338 isvo = createNewEntryInItemsTable(isvo);
339 itemId = isvo.getItemId();
341 logger.error("JDBC::getTable: Creating items entry for item '{}' failed.", itemName);
345 // Create the table name
346 logger.debug("JDBC::getTable: getTableName with rowId={} itemName={}", itemId, itemName);
347 tableName = namingStrategy.getTableName(itemId, itemName);
349 // Create table for item
350 String dataType = conf.getDBDAO().getDataType(item);
351 ivo = new ItemVO(tableName, itemName);
352 ivo.setDbType(dataType);
353 ivo = createItemTable(ivo);
354 logger.debug("JDBC::getTable: Table created for item '{}' with dataType {} in SQL database.", itemName,
357 itemNameToTableNameMap.put(itemName, tableName);
362 private void formatTableNames() throws JdbcSQLException {
363 boolean tmpinit = initialized;
368 List<ItemsVO> itemIdTableNames = ifItemsTableExists() ? getItemIDTableNames() : new ArrayList<ItemsVO>();
369 var itemTables = getItemTables().stream().map(ItemsVO::getTableName).collect(Collectors.toList());
370 List<ItemVO> oldNewTableNames;
372 if (itemIdTableNames.isEmpty()) {
373 // Without mappings we can only migrate from direct item name to numeric mapping.
374 if (conf.getTableUseRealCaseSensitiveItemNames()) {
375 logger.info("JDBC::formatTableNames: Nothing to migrate.");
376 initialized = tmpinit;
379 oldNewTableNames = new ArrayList<>();
380 for (String itemName : itemTables) {
381 ItemsVO isvo = new ItemsVO();
382 isvo.setItemName(itemName);
383 isvo = createNewEntryInItemsTable(isvo);
384 int itemId = isvo.getItemId();
386 logger.error("JDBC::formatTableNames: Creating items entry for item '{}' failed.", itemName);
388 String newTableName = namingStrategy.getTableName(itemId, itemName);
389 oldNewTableNames.add(new ItemVO(itemName, newTableName));
390 logger.info("JDBC::formatTableNames: Table '{}' will be renamed to '{}'", itemName, newTableName);
394 String itemsManageTable = new ItemsVO().getItemsManageTable();
395 Map<Integer, String> itemIdToItemNameMap = new HashMap<>();
397 for (ItemsVO vo : itemIdTableNames) {
398 int itemId = vo.getItemId();
399 String itemName = vo.getItemName();
400 itemIdToItemNameMap.put(itemId, itemName);
403 oldNewTableNames = namingStrategy.prepareMigration(itemTables, itemIdToItemNameMap, itemsManageTable);
406 updateItemTableNames(oldNewTableNames);
407 logger.info("JDBC::formatTableNames: Finished updating {} item table names", oldNewTableNames.size());
409 initialized = tmpinit;
412 protected Set<PersistenceItemInfo> getItems() {
413 // TODO: in general it would be possible to query the count, earliest and latest values for each item too but it
414 // would be a very costly operation
415 return itemNameToTableNameMap.keySet().stream().map(itemName -> new JdbcPersistenceItemInfo(itemName))
416 .collect(Collectors.<PersistenceItemInfo> toSet());
422 private void logTime(String me, long timerStart, long timerStop) {
423 if (conf.enableLogTime && logger.isInfoEnabled()) {
425 int timerDiff = (int) (timerStop - timerStart);
426 if (timerDiff < afterAccessMin) {
427 afterAccessMin = timerDiff;
429 if (timerDiff > afterAccessMax) {
430 afterAccessMax = timerDiff;
432 conf.timeAverage50arr.add(timerDiff);
433 conf.timeAverage100arr.add(timerDiff);
434 conf.timeAverage200arr.add(timerDiff);
435 if (conf.timerCount == 1) {
436 conf.timer1000 = System.currentTimeMillis();
438 if (conf.timerCount == 1001) {
439 conf.time1000Statements = Math.round(((int) (System.currentTimeMillis() - conf.timer1000)) / 1000);// Seconds
443 "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",
444 me, timerDiff, conf.timeAverage50arr.getAverageInteger(),
445 conf.timeAverage100arr.getAverageInteger(), conf.timeAverage200arr.getAverageInteger(),
446 afterAccessMin, afterAccessMax, conf.time1000Statements, conf.timerCount);