/* * 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.AbstractObjectCache; 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.*; /** * Composite objects cache. * Each composite object consists from several rows. * Each row object refers to some other DB objects. * Each composite object belongs to some parent object (table usually) and it's name is unique within it's parent. * Each row object name is unique within main object. * * Examples: table index, constraint. */ public abstract class JDBCCompositeCache< OWNER extends DBSObject, PARENT extends DBSObject, OBJECT extends DBSObject, ROW_REF extends DBSObject> extends AbstractObjectCache<OWNER, OBJECT> { protected static final Log log = Log.getLog(JDBCCompositeCache.class); public static final String DEFAULT_OBJECT_NAME = "#DBOBJ"; private final JDBCStructCache<OWNER,?,?> parentCache; private final Class<PARENT> parentType; private final Object parentColumnName; private final Object objectColumnName; private final Map<PARENT, List<OBJECT>> objectCache = new IdentityHashMap<>(); protected JDBCCompositeCache( JDBCStructCache<OWNER,?,?> parentCache, Class<PARENT> parentType, Object parentColumnName, Object objectColumnName) { this.parentCache = parentCache; this.parentType = parentType; this.parentColumnName = parentColumnName; this.objectColumnName = objectColumnName; } @NotNull abstract protected JDBCStatement prepareObjectsStatement(JDBCSession session, OWNER owner, PARENT forParent) throws SQLException; @Nullable abstract protected OBJECT fetchObject(JDBCSession session, OWNER owner, PARENT parent, String childName, JDBCResultSet resultSet) throws SQLException, DBException; @Nullable abstract protected ROW_REF[] fetchObjectRow(JDBCSession session, PARENT parent, OBJECT forObject, JDBCResultSet resultSet) throws SQLException, DBException; protected PARENT getParent(OBJECT object) { return (PARENT) object.getParentObject(); } abstract protected void cacheChildren(DBRProgressMonitor monitor, OBJECT object, List<ROW_REF> children); // Second cache function. Needed for complex entities which refers to each other (foreign keys) // First cache must cache all unique constraint, second must cache foreign keys references which refers unique keys protected void cacheChildren2(DBRProgressMonitor monitor, OBJECT object, List<ROW_REF> children) { } @NotNull @Override public Collection<OBJECT> getAllObjects(@NotNull DBRProgressMonitor monitor, @Nullable OWNER owner) throws DBException { return getObjects(monitor, owner, null); } public Collection<OBJECT> getObjects(DBRProgressMonitor monitor, OWNER owner, PARENT forParent) throws DBException { loadObjects(monitor, owner, forParent); return getCachedObjects(forParent); } public <TYPE extends OBJECT> Collection<TYPE > getTypedObjects(DBRProgressMonitor monitor, OWNER owner, PARENT forParent, Class<TYPE> type) throws DBException { List<TYPE> result = new ArrayList<>(); Collection<OBJECT> objects = getObjects(monitor, owner, forParent); if (objects != null) { for (OBJECT object : objects) { if (type.isInstance(object)) { result.add(type.cast(object)); } } } return result; } public Collection<OBJECT> getCachedObjects(PARENT forParent) { if (forParent == null) { return getCachedObjects(); } else { synchronized (objectCache) { return objectCache.get(forParent); } } } @Override public OBJECT getObject(@NotNull DBRProgressMonitor monitor, @Nullable OWNER owner, @NotNull String objectName) throws DBException { loadObjects(monitor, owner, null); return getCachedObject(objectName); } public OBJECT getObject(DBRProgressMonitor monitor, OWNER owner, PARENT forParent, String objectName) throws DBException { loadObjects(monitor, owner, forParent); if (forParent == null) { return getCachedObject(objectName); } else { synchronized (objectCache) { return DBUtils.findObject(objectCache.get(forParent), objectName); } } } @Override public void cacheObject(@NotNull OBJECT object) { super.cacheObject(object); synchronized (objectCache) { PARENT parent = getParent(object); List<OBJECT> objects = objectCache.get(parent); if (objects == null) { objects = new ArrayList<>(); objectCache.put(parent, objects); } objects.add(object); } } @Override public void removeObject(@NotNull OBJECT object, boolean resetFullCache) { super.removeObject(object, resetFullCache); objectCache.remove(getParent(object)); } public void clearObjectCache(PARENT forParent) { if (forParent == null) { super.clearCache(); } else { objectCache.remove(forParent); } } public void setObjectCache(PARENT forParent, List<OBJECT> objects) { } @Override public void clearCache() { synchronized (objectCache) { this.objectCache.clear(); super.clearCache(); } } @Override public void setCache(List<OBJECT> objects) { super.setCache(objects); synchronized (objectCache) { objectCache.clear(); for (OBJECT object : objects) { PARENT parent = getParent(object); List<OBJECT> parentObjects = objectCache.get(parent); if (parentObjects == null) { parentObjects = new ArrayList<>(); objectCache.put(parent, parentObjects); } parentObjects.add(object); } } } private class ObjectInfo { final OBJECT object; final List<ROW_REF> rows = new ArrayList<>(); public boolean broken; public boolean needsCaching; public ObjectInfo(OBJECT object) { this.object = object; } } protected synchronized void loadObjects(DBRProgressMonitor monitor, OWNER owner, PARENT forParent) throws DBException { synchronized (objectCache) { if ((forParent == null && isFullyCached()) || (forParent != null && (!forParent.isPersisted() || objectCache.containsKey(forParent)))) { return; } } // Load tables and columns first if (forParent == null) { parentCache.loadObjects(monitor, owner); parentCache.loadChildren(monitor, owner, null); } Map<PARENT, Map<String, ObjectInfo>> parentObjectMap = new LinkedHashMap<>(); // Load index columns DBPDataSource dataSource = owner.getDataSource(); assert (dataSource != null); try (JDBCSession session = DBUtils.openMetaSession(monitor, dataSource, "Load composite objects")) { JDBCStatement dbStat = prepareObjectsStatement(session, owner, forParent); dbStat.setFetchSize(DBConstants.METADATA_FETCH_SIZE); try { dbStat.executeStatement(); JDBCResultSet dbResult = dbStat.getResultSet(); if (dbResult != null) try { while (dbResult.next()) { if (monitor.isCanceled()) { break; } String parentName = parentColumnName instanceof Number ? JDBCUtils.safeGetString(dbResult, ((Number)parentColumnName).intValue()) : JDBCUtils.safeGetString(dbResult, parentColumnName.toString()); String objectName = objectColumnName instanceof Number ? JDBCUtils.safeGetString(dbResult, ((Number)objectColumnName).intValue()) : JDBCUtils.safeGetString(dbResult, objectColumnName.toString()); if (CommonUtils.isEmpty(objectName)) { // Use default name objectName = getDefaultObjectName(dbResult, parentName); } if (forParent == null && CommonUtils.isEmpty(parentName)) { // No parent - can't evaluate it log.debug("Empty parent name in " + this); continue; } PARENT parent = forParent; if (parent == null) { parent = parentCache.getObject(monitor, owner, parentName, parentType); if (parent == null) { log.debug("Object '" + objectName + "' owner '" + parentName + "' not found"); continue; } } synchronized (objectCache) { if (objectCache.containsKey(parent)) { // Already cached continue; } } // Add to map Map<String, ObjectInfo> objectMap = parentObjectMap.get(parent); if (objectMap == null) { objectMap = new TreeMap<>(); parentObjectMap.put(parent, objectMap); } ObjectInfo objectInfo = objectMap.get(objectName); if (objectInfo == null) { OBJECT object = fetchObject(session, owner, parent, objectName, dbResult); if (object == null) { // Can't fetch object continue; } objectName = object.getName(); objectInfo = new ObjectInfo(object); objectMap.put(objectName, objectInfo); } ROW_REF[] rowRef = fetchObjectRow(session, parent, objectInfo.object, dbResult); if (rowRef == null || rowRef.length == 0) { // At least one of rows is broken. // So entire object is broken, let's just skip it. objectInfo.broken = true; //log.debug("Object '" + objectName + "' metadata corrupted - NULL child returned"); continue; } Collections.addAll(objectInfo.rows, rowRef); } } finally { dbResult.close(); } } finally { dbStat.close(); } } catch (SQLException ex) { throw new DBException(ex, dataSource); } if (monitor.isCanceled()) { return; } // Fill global cache synchronized (this) { synchronized (objectCache) { if (forParent != null || !parentObjectMap.isEmpty()) { if (forParent == null) { // Cache global object list List<OBJECT> globalCache = new ArrayList<>(); for (Map<String, ObjectInfo> objMap : parentObjectMap.values()) { if (objMap != null) { for (ObjectInfo info : objMap.values()) { if (!info.broken) { globalCache.add(info.object); } } } } // Save precached objects in global cache for (List<OBJECT> objects : objectCache.values()) { globalCache.addAll(objects); } // Add precached objects to global cache too super.setCache(globalCache); this.invalidateObjects(monitor, owner, new CacheIterator()); } } // Cache data in individual objects only if we have read something or have certain parent object // Otherwise we assume that this function is not supported for mass data reading // All objects are read. Now assign them to parents for (Map.Entry<PARENT, Map<String, ObjectInfo>> colEntry : parentObjectMap.entrySet()) { if (colEntry.getValue() == null || objectCache.containsKey(colEntry.getKey())) { // Do not overwrite this object's cache continue; } Collection<ObjectInfo> objectInfos = colEntry.getValue().values(); ArrayList<OBJECT> objects = new ArrayList<>(objectInfos.size()); for (ObjectInfo objectInfo : objectInfos) { objectInfo.needsCaching = true; objects.add(objectInfo.object); } objectCache.put(colEntry.getKey(), objects); } // Now set empty object list for other parents if (forParent == null) { for (PARENT tmpParent : parentCache.getTypedObjects(monitor, owner, parentType)) { if (!parentObjectMap.containsKey(tmpParent) && !objectCache.containsKey(tmpParent)) { objectCache.put(tmpParent, new ArrayList<OBJECT>()); } } } else if (!parentObjectMap.containsKey(forParent) && !objectCache.containsKey(forParent)) { objectCache.put(forParent, new ArrayList<OBJECT>()); } } // Cache children lists (we do it in the end because children caching may operate with other model objects) for (Map.Entry<PARENT, Map<String, ObjectInfo>> colEntry : parentObjectMap.entrySet()) { for (ObjectInfo objectInfo : colEntry.getValue().values()) { if (objectInfo.needsCaching) { cacheChildren(monitor, objectInfo.object, objectInfo.rows); } } } for (Map.Entry<PARENT, Map<String, ObjectInfo>> colEntry : parentObjectMap.entrySet()) { for (ObjectInfo objectInfo : colEntry.getValue().values()) { if (objectInfo.needsCaching) { cacheChildren2(monitor, objectInfo.object, objectInfo.rows); } } } } } protected String getDefaultObjectName(JDBCResultSet dbResult, String parentName) { return parentName == null ? DEFAULT_OBJECT_NAME : parentName.toUpperCase() + "_" + DEFAULT_OBJECT_NAME; } }