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.dto.ItemVO;
35 import org.openhab.persistence.jdbc.dto.ItemsVO;
36 import org.openhab.persistence.jdbc.dto.JdbcPersistenceItemInfo;
37 import org.slf4j.Logger;
38 import org.slf4j.LoggerFactory;
40 import com.zaxxer.hikari.pool.HikariPool.PoolInitializationException;
45 * @author Helmut Lehmeyer - Initial contribution
48 public class JdbcMapper {
49 private final Logger logger = LoggerFactory.getLogger(JdbcMapper.class);
51 private final TimeZoneProvider timeZoneProvider;
53 // Error counter - used to reconnect to database on error
55 protected boolean initialized = false;
56 protected @NonNullByDefault({}) JdbcConfiguration conf;
57 protected final Map<String, String> itemNameToTableNameMap = new HashMap<>();
58 protected @NonNullByDefault({}) NamingStrategy namingStrategy;
59 private long afterAccessMin = 10000;
60 private long afterAccessMax = 0;
62 public JdbcMapper(TimeZoneProvider timeZoneProvider) {
63 this.timeZoneProvider = timeZoneProvider;
69 public boolean pingDB() {
70 logger.debug("JDBC::pingDB");
72 long timerStart = System.currentTimeMillis();
73 if (openConnection()) {
74 if (conf.getDbName() == null) {
76 "JDBC::pingDB asking db for name as absolutely first db action, after connection is established.");
77 String dbName = conf.getDBDAO().doGetDB();
81 conf.setDbName(dbName);
82 ret = dbName.length() > 0;
85 final @Nullable Integer result = conf.getDBDAO().doPingDB();
86 ret = result != null && result > 0;
89 logTime("pingDB", timerStart, System.currentTimeMillis());
93 public String getDB() {
94 logger.debug("JDBC::getDB");
95 long timerStart = System.currentTimeMillis();
96 String res = conf.getDBDAO().doGetDB();
97 logTime("getDB", timerStart, System.currentTimeMillis());
98 return res != null ? res : "";
101 public boolean ifItemsTableExists() {
102 logger.debug("JDBC::ifItemsTableExists");
103 long timerStart = System.currentTimeMillis();
104 boolean res = conf.getDBDAO().doIfTableExists(new ItemsVO());
105 logTime("doIfTableExists", timerStart, System.currentTimeMillis());
109 public ItemsVO createNewEntryInItemsTable(ItemsVO vo) {
110 logger.debug("JDBC::createNewEntryInItemsTable");
111 long timerStart = System.currentTimeMillis();
112 Long i = conf.getDBDAO().doCreateNewEntryInItemsTable(vo);
113 vo.setItemId(i.intValue());
114 logTime("doCreateNewEntryInItemsTable", timerStart, System.currentTimeMillis());
118 public boolean createItemsTableIfNot(ItemsVO vo) {
119 logger.debug("JDBC::createItemsTableIfNot");
120 long timerStart = System.currentTimeMillis();
121 conf.getDBDAO().doCreateItemsTableIfNot(vo);
122 logTime("doCreateItemsTableIfNot", timerStart, System.currentTimeMillis());
126 public boolean dropItemsTableIfExists(ItemsVO vo) {
127 logger.debug("JDBC::dropItemsTableIfExists");
128 long timerStart = System.currentTimeMillis();
129 conf.getDBDAO().doDropItemsTableIfExists(vo);
130 logTime("doDropItemsTableIfExists", timerStart, System.currentTimeMillis());
134 public ItemsVO deleteItemsEntry(ItemsVO vo) {
135 logger.debug("JDBC::deleteItemsEntry");
136 long timerStart = System.currentTimeMillis();
137 conf.getDBDAO().doDeleteItemsEntry(vo);
138 logTime("deleteItemsEntry", timerStart, System.currentTimeMillis());
142 public List<ItemsVO> getItemIDTableNames() {
143 logger.debug("JDBC::getItemIDTableNames");
144 long timerStart = System.currentTimeMillis();
145 List<ItemsVO> vo = conf.getDBDAO().doGetItemIDTableNames(new ItemsVO());
146 logTime("getItemIDTableNames", timerStart, System.currentTimeMillis());
150 public List<ItemsVO> getItemTables() {
151 logger.debug("JDBC::getItemTables");
152 long timerStart = System.currentTimeMillis();
153 ItemsVO vo = new ItemsVO();
154 vo.setJdbcUriDatabaseName(conf.getDbName());
155 List<ItemsVO> vol = conf.getDBDAO().doGetItemTables(vo);
156 logTime("getItemTables", timerStart, System.currentTimeMillis());
163 public void updateItemTableNames(List<ItemVO> vol) {
164 logger.debug("JDBC::updateItemTableNames");
165 long timerStart = System.currentTimeMillis();
166 conf.getDBDAO().doUpdateItemTableNames(vol);
167 logTime("updateItemTableNames", timerStart, System.currentTimeMillis());
170 public ItemVO createItemTable(ItemVO vo) {
171 logger.debug("JDBC::createItemTable");
172 long timerStart = System.currentTimeMillis();
173 conf.getDBDAO().doCreateItemTable(vo);
174 logTime("createItemTable", timerStart, System.currentTimeMillis());
178 public Item storeItemValue(Item item, State itemState, @Nullable ZonedDateTime date) {
179 logger.debug("JDBC::storeItemValue: item={} state={} date={}", item, itemState, date);
180 String tableName = getTable(item);
181 long timerStart = System.currentTimeMillis();
183 conf.getDBDAO().doStoreItemValue(item, itemState, new ItemVO(tableName, null));
185 conf.getDBDAO().doStoreItemValue(item, itemState, new ItemVO(tableName, null), date);
187 logTime("storeItemValue", timerStart, System.currentTimeMillis());
192 public List<HistoricItem> getHistItemFilterQuery(FilterCriteria filter, int numberDecimalcount, String table,
195 "JDBC::getHistItemFilterQuery filter='{}' numberDecimalcount='{}' table='{}' item='{}' itemName='{}'",
196 true, numberDecimalcount, table, item, item.getName());
197 long timerStart = System.currentTimeMillis();
198 List<HistoricItem> result = conf.getDBDAO().doGetHistItemFilterQuery(item, filter, numberDecimalcount, table,
199 item.getName(), timeZoneProvider.getTimeZone());
200 logTime("getHistItemFilterQuery", timerStart, System.currentTimeMillis());
205 public boolean deleteItemValues(FilterCriteria filter, String table) {
206 logger.debug("JDBC::deleteItemValues filter='{}' table='{}' itemName='{}'", true, table, filter.getItemName());
207 long timerStart = System.currentTimeMillis();
208 conf.getDBDAO().doDeleteItemValues(filter, table, timeZoneProvider.getTimeZone());
209 logTime("deleteItemValues", timerStart, System.currentTimeMillis());
214 /***********************
215 * DATABASE CONNECTION *
216 ***********************/
217 protected boolean openConnection() {
218 logger.debug("JDBC::openConnection isDriverAvailable: {}", conf.isDriverAvailable());
219 if (conf.isDriverAvailable() && !conf.isDbConnected()) {
220 logger.info("JDBC::openConnection: Driver is available::Yank setupDataSource");
222 Yank.setupDefaultConnectionPool(conf.getHikariConfiguration());
223 conf.setDbConnected(true);
225 } catch (PoolInitializationException e) {
226 Throwable cause = e.getCause();
227 if (cause instanceof SQLInvalidAuthorizationSpecException) {
228 logger.warn("JDBC::openConnection: failed to open connection: {}", cause.getMessage());
230 logger.warn("JDBC::openConnection: failed to open connection: {}", e.getMessage());
235 } else if (!conf.isDriverAvailable()) {
236 logger.warn("JDBC::openConnection: no driver available!");
243 protected void closeConnection() {
244 logger.debug("JDBC::closeConnection");
245 // Closes all open connection pools
246 Yank.releaseDefaultConnectionPool();
247 conf.setDbConnected(false);
250 protected boolean checkDBAccessability() {
251 // Check if connection is valid
256 boolean p = pingDB();
258 logger.debug("JDBC::checkDBAcessability, first try connection: {}", p);
259 return (p && !(conf.getErrReconnectThreshold() > 0 && errCnt <= conf.getErrReconnectThreshold()));
263 logger.debug("JDBC::checkDBAcessability, second try connection: {}", p);
264 return (p && !(conf.getErrReconnectThreshold() > 0 && errCnt <= conf.getErrReconnectThreshold()));
268 /**************************
269 * DATABASE TABLEHANDLING *
270 **************************/
271 protected void checkDBSchema() {
272 if (!conf.getTableUseRealCaseSensitiveItemNames()) {
273 createItemsTableIfNot(new ItemsVO());
275 if (conf.getRebuildTableNames()) {
278 if (conf.getTableUseRealCaseSensitiveItemNames()) {
279 dropItemsTableIfExists(new ItemsVO());
282 "JDBC::checkDBSchema: Rebuild complete, configure the 'rebuildTableNames' setting to 'false' to stop rebuilds on startup");
283 // Reset the error counter
286 populateItemNameToTableNameMap();
289 private void populateItemNameToTableNameMap() {
290 itemNameToTableNameMap.clear();
291 if (conf.getTableUseRealCaseSensitiveItemNames()) {
292 for (String itemName : getItemTables().stream().map(t -> t.getTableName()).collect(Collectors.toList())) {
293 itemNameToTableNameMap.put(itemName, itemName);
296 for (ItemsVO vo : getItemIDTableNames()) {
297 itemNameToTableNameMap.put(vo.getItemName(),
298 namingStrategy.getTableName(vo.getItemId(), vo.getItemName()));
303 protected String getTable(Item item) {
308 String itemName = item.getName();
309 String tableName = itemNameToTableNameMap.get(itemName);
311 // Table already exists - return the name
312 if (!Objects.isNull(tableName)) {
316 logger.debug("JDBC::getTable: no table found for item '{}' in sqlTables", itemName);
318 if (!conf.getTableUseRealCaseSensitiveItemNames()) {
319 // Create a new entry in items table
320 isvo = new ItemsVO();
321 isvo.setItemName(itemName);
322 isvo = createNewEntryInItemsTable(isvo);
323 itemId = isvo.getItemId();
325 logger.error("JDBC::getTable: Creating items entry for item '{}' failed.", itemName);
329 // Create the table name
330 logger.debug("JDBC::getTable: getTableName with rowId={} itemName={}", itemId, itemName);
331 tableName = namingStrategy.getTableName(itemId, itemName);
333 // Create table for item
334 String dataType = conf.getDBDAO().getDataType(item);
335 ivo = new ItemVO(tableName, itemName);
336 ivo.setDbType(dataType);
337 ivo = createItemTable(ivo);
338 logger.debug("JDBC::getTable: Table created for item '{}' with dataType {} in SQL database.", itemName,
341 itemNameToTableNameMap.put(itemName, tableName);
346 private void formatTableNames() {
347 boolean tmpinit = initialized;
352 List<ItemsVO> itemIdTableNames = ifItemsTableExists() ? getItemIDTableNames() : new ArrayList<ItemsVO>();
353 List<String> itemTables = getItemTables().stream().map(t -> t.getTableName()).collect(Collectors.toList());
354 List<ItemVO> oldNewTableNames;
356 if (itemIdTableNames.isEmpty()) {
357 // Without mappings we can only migrate from direct item name to numeric mapping.
358 if (conf.getTableUseRealCaseSensitiveItemNames()) {
359 logger.info("JDBC::formatTableNames: Nothing to migrate.");
360 initialized = tmpinit;
363 oldNewTableNames = new ArrayList<>();
364 for (String itemName : itemTables) {
365 ItemsVO isvo = new ItemsVO();
366 isvo.setItemName(itemName);
367 isvo = createNewEntryInItemsTable(isvo);
368 int itemId = isvo.getItemId();
370 logger.error("JDBC::formatTableNames: Creating items entry for item '{}' failed.", itemName);
372 String newTableName = namingStrategy.getTableName(itemId, itemName);
373 oldNewTableNames.add(new ItemVO(itemName, newTableName));
374 logger.info("JDBC::formatTableNames: Table '{}' will be renamed to '{}'", itemName, newTableName);
378 String itemsManageTable = new ItemsVO().getItemsManageTable();
379 Map<Integer, String> itemIdToItemNameMap = new HashMap<>();
381 for (ItemsVO vo : itemIdTableNames) {
382 int itemId = vo.getItemId();
383 String itemName = vo.getItemName();
384 itemIdToItemNameMap.put(itemId, itemName);
387 oldNewTableNames = namingStrategy.prepareMigration(itemTables, itemIdToItemNameMap, itemsManageTable);
390 updateItemTableNames(oldNewTableNames);
391 logger.info("JDBC::formatTableNames: Finished updating {} item table names", oldNewTableNames.size());
393 initialized = tmpinit;
396 public Set<PersistenceItemInfo> getItems() {
397 // TODO: in general it would be possible to query the count, earliest and latest values for each item too but it
398 // would be a very costly operation
399 return itemNameToTableNameMap.keySet().stream().map(itemName -> new JdbcPersistenceItemInfo(itemName))
400 .collect(Collectors.<PersistenceItemInfo> toSet());
406 private void logTime(String me, long timerStart, long timerStop) {
407 if (conf.enableLogTime && logger.isInfoEnabled()) {
409 int timerDiff = (int) (timerStop - timerStart);
410 if (timerDiff < afterAccessMin) {
411 afterAccessMin = timerDiff;
413 if (timerDiff > afterAccessMax) {
414 afterAccessMax = timerDiff;
416 conf.timeAverage50arr.add(timerDiff);
417 conf.timeAverage100arr.add(timerDiff);
418 conf.timeAverage200arr.add(timerDiff);
419 if (conf.timerCount == 1) {
420 conf.timer1000 = System.currentTimeMillis();
422 if (conf.timerCount == 1001) {
423 conf.time1000Statements = Math.round(((int) (System.currentTimeMillis() - conf.timer1000)) / 1000);// Seconds
427 "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",
428 me, timerDiff, conf.timeAverage50arr.getAverageInteger(),
429 conf.timeAverage100arr.getAverageInteger(), conf.timeAverage200arr.getAverageInteger(),
430 afterAccessMin, afterAccessMax, conf.time1000Statements, conf.timerCount);