/* * DBeaver - Universal Database Manager * Copyright (C) 2010-2017 Serge Rider (serge@jkiss.org) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.jkiss.dbeaver.model.impl.jdbc.cache; import org.jkiss.code.NotNull; import org.jkiss.code.Nullable; import org.jkiss.dbeaver.DBException; import org.jkiss.dbeaver.Log; import org.jkiss.dbeaver.model.DBConstants; import org.jkiss.dbeaver.model.DBPDataSource; import org.jkiss.dbeaver.model.DBUtils; import org.jkiss.dbeaver.model.exec.jdbc.JDBCResultSet; import org.jkiss.dbeaver.model.exec.jdbc.JDBCSession; import org.jkiss.dbeaver.model.exec.jdbc.JDBCStatement; import org.jkiss.dbeaver.model.impl.DBSObjectCache; import org.jkiss.dbeaver.model.impl.DBSStructCache; import org.jkiss.dbeaver.model.impl.SimpleObjectCache; import org.jkiss.dbeaver.model.impl.jdbc.JDBCUtils; import org.jkiss.dbeaver.model.runtime.DBRProgressMonitor; import org.jkiss.dbeaver.model.struct.DBSObject; import org.jkiss.utils.CommonUtils; import java.sql.SQLException; import java.util.*; /** * JDBC structured objects cache. * Stores objects themselves and their child objects. */ public abstract class JDBCStructCache<OWNER extends DBSObject, OBJECT extends DBSObject, CHILD extends DBSObject> extends JDBCObjectCache<OWNER, OBJECT> implements DBSStructCache<OWNER, OBJECT, CHILD> { private static final Log log = Log.getLog(JDBCStructCache.class); private final Object objectNameColumn; private volatile boolean childrenCached = false; private final Map<OBJECT, SimpleObjectCache<OBJECT, CHILD>> childrenCache = new IdentityHashMap<>(); abstract protected JDBCStatement prepareChildrenStatement(@NotNull JDBCSession session, @NotNull OWNER owner, @Nullable OBJECT forObject) throws SQLException; abstract protected CHILD fetchChild(@NotNull JDBCSession session, @NotNull OWNER owner, @NotNull OBJECT parent, @NotNull JDBCResultSet dbResult) throws SQLException, DBException; protected JDBCStructCache(Object objectNameColumn) { this.objectNameColumn = objectNameColumn; } /** * Reads children objects from database * * @param monitor * monitor * @param forObject * object for which to read children. If null then reads children for all objects in this container. * @throws org.jkiss.dbeaver.DBException * on error */ public synchronized void loadChildren(DBRProgressMonitor monitor, OWNER owner, @Nullable final OBJECT forObject) throws DBException { if ((forObject == null && this.childrenCached) || (forObject != null && (!forObject.isPersisted() || isChildrenCached(forObject))) || monitor.isCanceled()) { return; } if (forObject == null) { // If we have some child objects read before that - do not clear them. // We have to reuse them because there could be some references in cached model //clearChildrenCache(null); super.loadObjects(monitor, owner); } DBPDataSource dataSource = owner.getDataSource(); if (dataSource == null) { throw new DBException("Not connected to database"); } try (JDBCSession session = DBUtils.openMetaSession(monitor, dataSource, "Load child objects")) { Map<OBJECT, List<CHILD>> objectMap = new HashMap<>(); // Load columns try (JDBCStatement dbStat = prepareChildrenStatement(session, owner, forObject)) { dbStat.setFetchSize(DBConstants.METADATA_FETCH_SIZE); dbStat.executeStatement(); JDBCResultSet dbResult = dbStat.getResultSet(); if (dbResult != null) { try { while (dbResult.next()) { if (monitor.isCanceled()) { break; } String objectName; if (objectNameColumn instanceof Number) { objectName = JDBCUtils.safeGetString(dbResult, ((Number) objectNameColumn).intValue()); } else { objectName = JDBCUtils.safeGetStringTrimmed(dbResult, objectNameColumn.toString()); } if (objectName == null) { log.debug("NULL object name in " + this); continue; } OBJECT object = forObject; if (object == null) { object = super.getCachedObject(objectName); if (object == null) { log.debug("Object '" + objectName + "' not found"); continue; } } if (isChildrenCached(object)) { // Already read continue; } CHILD child = fetchChild(session, owner, object, dbResult); if (child == null) { continue; } // Add to map List<CHILD> children = objectMap.get(object); if (children == null) { children = new ArrayList<>(); objectMap.put(object, children); } children.add(child); } if (monitor.isCanceled()) { return; } // All children are read. Now assign them to parents for (Map.Entry<OBJECT, List<CHILD>> colEntry : objectMap.entrySet()) { if (!isChildrenCached(colEntry.getKey())) { // isChildrenCached may return true if the same cache was read in other thread // just skip cacheChildren(colEntry.getKey(), colEntry.getValue()); } } if (forObject == null) { if (objectMap.isEmpty()) { // Nothing was read. May be it means empty list of children // but possibly this feature is not supported [JDBC: SQLite] } else { // Now set empty column list for other tables for (OBJECT tmpObject : getAllObjects(monitor, owner)) { if (!isChildrenCached(tmpObject) && !objectMap.containsKey(tmpObject)) { cacheChildren(tmpObject, new ArrayList<CHILD>()); } } this.childrenCached = true; } } else if (!objectMap.containsKey(forObject)) { cacheChildren(forObject, new ArrayList<CHILD>()); } } finally { dbResult.close(); } } } } catch (SQLException ex) { throw new DBException(ex, dataSource); } } @Override public void removeObject(@NotNull OBJECT object, boolean resetFullCache) { super.removeObject(object, resetFullCache); clearChildrenCache(object); } @Override public void clearCache() { this.clearChildrenCache(null); super.clearCache(); } /** * Returns cache for child objects. Creates cache i it doesn't exists * * @param forObject * parent object * @return cache */ public DBSObjectCache<OBJECT, CHILD> getChildrenCache(final OBJECT forObject) { synchronized (childrenCache) { SimpleObjectCache<OBJECT, CHILD> nestedCache = childrenCache.get(forObject); if (nestedCache == null) { // Create new empty children cache // This may happen only when invoked for newly created object (e.g. when we create new column // in a new created table) nestedCache = new SimpleObjectCache<>(); nestedCache.setCache(new ArrayList<CHILD>()); childrenCache.put(forObject, nestedCache); } return nestedCache; } } @Nullable public List<CHILD> getChildren(DBRProgressMonitor monitor, OWNER owner, final OBJECT forObject) throws DBException { loadChildren(monitor, owner, forObject); synchronized (childrenCache) { SimpleObjectCache<OBJECT, CHILD> nestedCache = childrenCache.get(forObject); return nestedCache == null ? null : nestedCache.getAllObjects(monitor, null); } } @Nullable public CHILD getChild(DBRProgressMonitor monitor, OWNER owner, final OBJECT forObject, String objectName) throws DBException { loadChildren(monitor, owner, forObject); synchronized (childrenCache) { SimpleObjectCache<OBJECT, CHILD> nestedCache = childrenCache.get(forObject); return nestedCache == null ? null : nestedCache.getObject(monitor, forObject, objectName); } } public void clearChildrenCache(OBJECT forParent) { synchronized (childrenCache) { if (forParent != null) { this.childrenCache.remove(forParent); } else { this.childrenCache.clear(); } childrenCached = false; } } protected boolean isChildrenCached(OBJECT parent) { synchronized (childrenCache) { return childrenCache.containsKey(parent); // SimpleObjectCache<OBJECT, CHILD> chCache = childrenCache.get(parent); // return chCache != null && !CommonUtils.isEmpty(chCache.getCachedObjects()); } } protected void cacheChildren(OBJECT parent, List<CHILD> children) { synchronized (childrenCache) { SimpleObjectCache<OBJECT, CHILD> nestedCache = childrenCache.get(parent); if (nestedCache == null) { nestedCache = new SimpleObjectCache<>(); nestedCache.setCaseSensitive(caseSensitive); childrenCache.put(parent, nestedCache); } nestedCache.setCache(children); } } }