2 * Copyright (c) 2010-2024 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.time.ZonedDateTime;
16 import java.util.ArrayList;
17 import java.util.Collection;
18 import java.util.Date;
19 import java.util.List;
20 import java.util.Locale;
22 import java.util.Map.Entry;
24 import java.util.concurrent.Executors;
25 import java.util.concurrent.ScheduledExecutorService;
26 import java.util.stream.Collectors;
28 import org.eclipse.jdt.annotation.NonNullByDefault;
29 import org.eclipse.jdt.annotation.Nullable;
30 import org.openhab.core.common.NamedThreadFactory;
31 import org.openhab.core.config.core.ConfigurableService;
32 import org.openhab.core.i18n.TimeZoneProvider;
33 import org.openhab.core.items.GroupItem;
34 import org.openhab.core.items.Item;
35 import org.openhab.core.items.ItemNotFoundException;
36 import org.openhab.core.items.ItemRegistry;
37 import org.openhab.core.persistence.FilterCriteria;
38 import org.openhab.core.persistence.HistoricItem;
39 import org.openhab.core.persistence.ModifiablePersistenceService;
40 import org.openhab.core.persistence.PersistenceItemInfo;
41 import org.openhab.core.persistence.PersistenceService;
42 import org.openhab.core.persistence.QueryablePersistenceService;
43 import org.openhab.core.persistence.strategy.PersistenceStrategy;
44 import org.openhab.core.types.State;
45 import org.openhab.core.types.UnDefType;
46 import org.openhab.persistence.jdbc.internal.db.JdbcBaseDAO;
47 import org.openhab.persistence.jdbc.internal.dto.Column;
48 import org.openhab.persistence.jdbc.internal.dto.ItemsVO;
49 import org.openhab.persistence.jdbc.internal.exceptions.JdbcException;
50 import org.openhab.persistence.jdbc.internal.exceptions.JdbcSQLException;
51 import org.osgi.framework.BundleContext;
52 import org.osgi.framework.Constants;
53 import org.osgi.service.component.annotations.Activate;
54 import org.osgi.service.component.annotations.Component;
55 import org.osgi.service.component.annotations.Deactivate;
56 import org.osgi.service.component.annotations.Reference;
57 import org.slf4j.Logger;
58 import org.slf4j.LoggerFactory;
61 * This is the implementation of the JDBC {@link PersistenceService}.
63 * @author Helmut Lehmeyer - Initial contribution
64 * @author Kai Kreuzer - Migration to 3.x
67 @Component(service = { PersistenceService.class,
68 QueryablePersistenceService.class }, configurationPid = "org.openhab.jdbc", //
69 property = Constants.SERVICE_PID + "=org.openhab.jdbc")
70 @ConfigurableService(category = "persistence", label = "JDBC Persistence Service", description_uri = JdbcPersistenceServiceConstants.CONFIG_URI)
71 public class JdbcPersistenceService extends JdbcMapper implements ModifiablePersistenceService {
73 private final Logger logger = LoggerFactory.getLogger(JdbcPersistenceService.class);
75 private final ItemRegistry itemRegistry;
77 private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1,
78 new NamedThreadFactory(JdbcPersistenceServiceConstants.SERVICE_ID));
81 public JdbcPersistenceService(final @Reference ItemRegistry itemRegistry,
82 final @Reference TimeZoneProvider timeZoneProvider) {
83 super(timeZoneProvider);
84 this.itemRegistry = itemRegistry;
88 * Called by the SCR to activate the component with its configuration read
91 * @param bundleContext
92 * BundleContext of the Bundle that defines this component
93 * @param configuration
94 * Configuration properties for this component obtained from the
98 public void activate(BundleContext bundleContext, Map<Object, Object> configuration) {
99 logger.debug("JDBC::activate: persistence service activated");
100 updateConfig(configuration);
104 * Called by the SCR to deactivate the component when either the
105 * configuration is removed or mandatory references are no longer satisfied
106 * or the component has simply been stopped.
109 * Reason code for the deactivation:<br>
111 * <li>0 – Unspecified
112 * <li>1 – The component was disabled
113 * <li>2 – A reference became unsatisfied
114 * <li>3 – A configuration was changed
115 * <li>4 – A configuration was deleted
116 * <li>5 – The component was disposed
117 * <li>6 – The bundle was stopped
121 public void deactivate(final int reason) {
122 logger.debug("JDBC::deactivate: persistence bundle stopping. Disconnecting from database. reason={}", reason);
123 // closeConnection();
128 public String getId() {
129 logger.debug("JDBC::getName: returning name 'jdbc' for queryable persistence service.");
130 return JdbcPersistenceServiceConstants.SERVICE_ID;
134 public String getLabel(@Nullable Locale locale) {
135 return JdbcPersistenceServiceConstants.SERVICE_LABEL;
139 public void store(Item item) {
140 scheduler.execute(() -> internalStore(item, null, item.getState()));
144 public void store(Item item, @Nullable String alias) {
145 // alias is not supported
146 scheduler.execute(() -> internalStore(item, null, item.getState()));
150 public void store(Item item, ZonedDateTime date, State state) {
151 scheduler.execute(() -> internalStore(item, date, state));
155 public void store(Item item, ZonedDateTime date, State state, @Nullable String alias) {
156 // alias is not supported
157 scheduler.execute(() -> internalStore(item, null, item.getState()));
160 private synchronized void internalStore(Item item, @Nullable ZonedDateTime date, State state) {
161 // Do not store undefined/uninitialized data
162 if (state instanceof UnDefType) {
163 logger.debug("JDBC::store: ignore Item '{}' because it is UnDefType", item.getName());
166 if (!checkDBAccessability()) {
168 "JDBC::store: No connection to database. Cannot persist state '{}' for item '{}'! Will retry connecting to database when error count:{} equals errReconnectThreshold:{}",
169 state, item, errCnt, conf.getErrReconnectThreshold());
173 long timerStart = System.currentTimeMillis();
174 storeItemValue(item, state, date);
175 if (logger.isDebugEnabled()) {
176 logger.debug("JDBC: Stored item '{}' as '{}' in SQL database at {} in {} ms.", item.getName(), state,
177 new Date(), System.currentTimeMillis() - timerStart);
179 } catch (JdbcException e) {
180 logger.warn("JDBC::store: Unable to store item", e);
185 public Set<PersistenceItemInfo> getItemInfo() {
190 * Queries the {@link PersistenceService} for data with a given filter
194 * the filter to apply to the query
195 * @return a time series of items
198 public Iterable<HistoricItem> query(FilterCriteria filter) {
199 if (!checkDBAccessability()) {
200 logger.warn("JDBC::query: database not connected, query aborted for item '{}'", filter.getItemName());
204 // Get the item name from the filter
205 // Also get the Item object so we can determine the type
207 String itemName = filter.getItemName();
208 if (itemName == null) {
209 logger.warn("Item name is missing in filter {}", filter);
212 logger.debug("JDBC::query: item is {}", itemName);
214 item = itemRegistry.getItem(itemName);
215 } catch (ItemNotFoundException e1) {
216 logger.error("JDBC::query: unable to get item for itemName: '{}'. Ignore and give up!", itemName);
220 if (item instanceof GroupItem) {
221 // For Group Item is BaseItem needed to get correct Type of Value.
222 item = GroupItem.class.cast(item).getBaseItem();
223 logger.debug("JDBC::query: item is instanceof GroupItem '{}'", itemName);
225 logger.debug("JDBC::query: BaseItem of GroupItem is null. Ignore and give up!");
228 if (item instanceof GroupItem) {
229 logger.debug("JDBC::query: BaseItem of GroupItem is a GroupItem too. Ignore and give up!");
234 String table = itemNameToTableNameMap.get(itemName);
236 logger.debug("JDBC::query: unable to find table for item with name: '{}', no data in database.", itemName);
241 long timerStart = System.currentTimeMillis();
242 List<HistoricItem> items = getHistItemFilterQuery(filter, conf.getNumberDecimalcount(), table, item);
243 if (logger.isDebugEnabled()) {
244 logger.debug("JDBC: Query for item '{}' returned {} rows in {} ms", itemName, items.size(),
245 System.currentTimeMillis() - timerStart);
250 } catch (JdbcSQLException e) {
251 logger.warn("JDBC::query: Unable to query item", e);
256 public void updateConfig(Map<Object, Object> configuration) {
257 logger.debug("JDBC::updateConfig");
259 conf = new JdbcConfiguration(configuration);
260 if (conf.valid && checkDBAccessability()) {
261 namingStrategy = new NamingStrategy(conf);
264 // connection has been established ... initialization completed!
266 } catch (JdbcSQLException e) {
267 logger.error("Failed to check database schema", e);
274 logger.debug("JDBC::updateConfig: configuration complete for service={}.", getId());
278 public List<PersistenceStrategy> getDefaultStrategies() {
279 return List.of(PersistenceStrategy.Globals.CHANGE);
283 public boolean remove(FilterCriteria filter) throws IllegalArgumentException {
284 if (!checkDBAccessability()) {
285 logger.warn("JDBC::remove: database not connected, remove aborted for item '{}'", filter.getItemName());
289 // Get the item name from the filter
290 // Also get the Item object so we can determine the type
291 String itemName = filter.getItemName();
292 logger.debug("JDBC::remove: item is {}", itemName);
293 if (itemName == null) {
294 throw new IllegalArgumentException("Item name must not be null");
297 String table = itemNameToTableNameMap.get(itemName);
299 logger.debug("JDBC::remove: unable to find table for item with name: '{}', no data in database.", itemName);
304 long timerStart = System.currentTimeMillis();
305 deleteItemValues(filter, table);
306 if (logger.isDebugEnabled()) {
307 logger.debug("JDBC: Deleted values for item '{}' in SQL database at {} in {} ms.", itemName, new Date(),
308 System.currentTimeMillis() - timerStart);
311 } catch (JdbcSQLException e) {
312 logger.debug("JDBC::remove: Unable to remove values for item", e);
318 * Get a list of names of persisted items.
320 public Collection<String> getItemNames() {
321 return itemNameToTableNameMap.keySet();
325 * Get a map of item names to table names.
327 public Map<String, String> getItemNameToTableNameMap() {
328 return itemNameToTableNameMap;
332 * Check schema of specific item table for integrity issues.
334 * @param tableName for which columns should be checked
335 * @param itemName that corresponds to table
336 * @return Collection of strings, each describing an identified issue
337 * @throws JdbcSQLException on SQL errors
339 public Collection<String> getSchemaIssues(String tableName, String itemName) throws JdbcSQLException {
340 List<String> issues = new ArrayList<>();
342 if (!checkDBAccessability()) {
343 logger.warn("JDBC::getSchemaIssues: database not connected");
349 item = itemRegistry.getItem(itemName);
350 } catch (ItemNotFoundException e) {
353 JdbcBaseDAO dao = conf.getDBDAO();
354 String timeDataType = dao.sqlTypes.get("tablePrimaryKey");
355 if (timeDataType == null) {
358 String valueDataType = dao.getDataType(item);
359 List<Column> columns = getTableColumns(tableName);
360 for (Column column : columns) {
361 String columnName = column.getColumnName();
362 if ("time".equalsIgnoreCase(columnName)) {
363 if (!"time".equals(columnName)) {
364 issues.add("Column name 'time' expected, but is '" + columnName + "'");
366 if (!timeDataType.equalsIgnoreCase(column.getColumnType())
367 && !timeDataType.equalsIgnoreCase(column.getColumnTypeAlias())) {
368 issues.add("Column type '" + timeDataType + "' expected, but is '"
369 + column.getColumnType().toUpperCase() + "'");
371 if (column.getIsNullable()) {
372 issues.add("Column 'time' expected to be NOT NULL, but is nullable");
374 } else if ("value".equalsIgnoreCase(columnName)) {
375 if (!"value".equals(columnName)) {
376 issues.add("Column name 'value' expected, but is '" + columnName + "'");
378 if (!valueDataType.equalsIgnoreCase(column.getColumnType())
379 && !valueDataType.equalsIgnoreCase(column.getColumnTypeAlias())) {
380 issues.add("Column type '" + valueDataType + "' expected, but is '"
381 + column.getColumnType().toUpperCase() + "'");
383 if (!column.getIsNullable()) {
384 issues.add("Column 'value' expected to be nullable, but is NOT NULL");
387 issues.add("Column '" + columnName + "' not expected");
396 * @param tableName for which columns should be repaired
397 * @param itemName that corresponds to table
398 * @return true if table was altered, otherwise false
399 * @throws JdbcSQLException on SQL errors
401 public boolean fixSchemaIssues(String tableName, String itemName) throws JdbcSQLException {
402 if (!checkDBAccessability()) {
403 logger.warn("JDBC::fixSchemaIssues: database not connected");
409 item = itemRegistry.getItem(itemName);
410 } catch (ItemNotFoundException e) {
413 JdbcBaseDAO dao = conf.getDBDAO();
414 String timeDataType = dao.sqlTypes.get("tablePrimaryKey");
415 if (timeDataType == null) {
418 String valueDataType = dao.getDataType(item);
419 List<Column> columns = getTableColumns(tableName);
420 boolean isFixed = false;
421 for (Column column : columns) {
422 String columnName = column.getColumnName();
423 if ("time".equalsIgnoreCase(columnName)) {
424 if (!"time".equals(columnName)
425 || (!timeDataType.equalsIgnoreCase(column.getColumnType())
426 && !timeDataType.equalsIgnoreCase(column.getColumnTypeAlias()))
427 || column.getIsNullable()) {
428 alterTableColumn(tableName, "time", timeDataType, false);
431 } else if ("value".equalsIgnoreCase(columnName)) {
432 if (!"value".equals(columnName)
433 || (!valueDataType.equalsIgnoreCase(column.getColumnType())
434 && !valueDataType.equalsIgnoreCase(column.getColumnTypeAlias()))
435 || !column.getIsNullable()) {
436 alterTableColumn(tableName, "value", valueDataType, true);
445 * Get a list of all items with corresponding tables and an {@link ItemTableCheckEntryStatus} indicating
448 * @return list of {@link ItemTableCheckEntry}
450 public List<ItemTableCheckEntry> getCheckedEntries() throws JdbcSQLException {
451 List<ItemTableCheckEntry> entries = new ArrayList<>();
453 if (!checkDBAccessability()) {
454 logger.warn("JDBC::getCheckedEntries: database not connected");
458 var orphanTables = getItemTables().stream().map(ItemsVO::getTableName).collect(Collectors.toSet());
459 for (Entry<String, String> entry : itemNameToTableNameMap.entrySet()) {
460 String itemName = entry.getKey();
461 String tableName = entry.getValue();
462 entries.add(getCheckedEntry(itemName, tableName, orphanTables.contains(tableName)));
463 orphanTables.remove(tableName);
465 for (String orphanTable : orphanTables) {
466 entries.add(new ItemTableCheckEntry("", orphanTable, ItemTableCheckEntryStatus.ORPHAN_TABLE));
471 private ItemTableCheckEntry getCheckedEntry(String itemName, String tableName, boolean tableExists) {
474 itemRegistry.getItem(itemName);
476 } catch (ItemNotFoundException e) {
480 ItemTableCheckEntryStatus status;
483 status = ItemTableCheckEntryStatus.TABLE_MISSING;
485 status = ItemTableCheckEntryStatus.ITEM_AND_TABLE_MISSING;
487 } else if (itemExists) {
488 status = ItemTableCheckEntryStatus.VALID;
490 status = ItemTableCheckEntryStatus.ITEM_MISSING;
492 return new ItemTableCheckEntry(itemName, tableName, status);
496 * Clean up inconsistent item: Remove from index and drop table.
497 * Tables with any rows are skipped, unless force is set.
499 * @param itemName Name of item to clean
500 * @param force If true, non-empty tables will be dropped too
501 * @return true if item was cleaned up
502 * @throws JdbcSQLException
504 public boolean cleanupItem(String itemName, boolean force) throws JdbcSQLException {
505 if (!checkDBAccessability()) {
506 logger.warn("JDBC::cleanupItem: database not connected");
510 String tableName = itemNameToTableNameMap.get(itemName);
511 if (tableName == null) {
514 ItemTableCheckEntry entry = getCheckedEntry(itemName, tableName, ifTableExists(tableName));
515 return cleanupItem(entry, force);
519 * Clean up inconsistent item: Remove from index and drop table.
520 * Tables with any rows are skipped.
523 * @return true if item was cleaned up
524 * @throws JdbcSQLException
526 public boolean cleanupItem(ItemTableCheckEntry entry) throws JdbcSQLException {
527 return cleanupItem(entry, false);
530 private boolean cleanupItem(ItemTableCheckEntry entry, boolean force) throws JdbcSQLException {
531 if (!checkDBAccessability()) {
532 logger.warn("JDBC::cleanupItem: database not connected");
536 ItemTableCheckEntryStatus status = entry.getStatus();
537 String tableName = entry.getTableName();
540 if (!force && getRowCount(tableName) > 0) {
543 dropTable(tableName);
544 // Fall through to remove from index.
546 case ITEM_AND_TABLE_MISSING:
547 if (!conf.getTableUseRealCaseSensitiveItemNames()) {
548 ItemsVO itemsVo = new ItemsVO();
549 itemsVo.setItemName(entry.getItemName());
550 itemsVo.setItemsManageTable(conf.getItemsManageTable());
551 deleteItemsEntry(itemsVo);
553 itemNameToTableNameMap.remove(entry.getItemName());