package uk.ac.ebi.fg.myequivalents.dao; import java.io.OutputStream; import java.math.BigDecimal; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.util.ArrayList; import java.util.Date; import java.util.LinkedList; import java.util.List; import java.util.Random; import javax.persistence.EntityManager; import javax.persistence.Query; import javax.xml.bind.Marshaller; import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.Validate; import org.hibernate.CacheMode; import org.hibernate.SQLQuery; import org.hibernate.ScrollMode; import org.hibernate.ScrollableResults; import org.hibernate.Session; import org.hibernate.jdbc.Work; import org.hibernate.jpa.HibernateEntityManager; import org.hibernate.type.StringType; import uk.ac.ebi.fg.myequivalents.managers.interfaces.EntityMappingManager; import uk.ac.ebi.fg.myequivalents.managers.interfaces.EntityMappingSearchResult; import uk.ac.ebi.fg.myequivalents.managers.interfaces.EntityMappingSearchResult.Bundle; import uk.ac.ebi.fg.myequivalents.model.Entity; import uk.ac.ebi.fg.myequivalents.model.EntityId; import uk.ac.ebi.fg.myequivalents.model.EntityMapping; import uk.ac.ebi.fg.myequivalents.model.Service; import uk.ac.ebi.fg.myequivalents.utils.DbEntityIdResolver; import uk.ac.ebi.fg.myequivalents.utils.jaxb.JAXBUtils; import uk.ac.ebi.utils.security.IdUtils; /** * * The DAO for {@link EntityMapping}. This manages basic operations and, as any other DAOs, delegates transaction * managements to the outside. * * TODO: factorise the queries. * * <dl><dt>date</dt><dd>May 25, 2012</dd></dl> * @author Marco Brandizi * */ public class EntityMappingDAO extends AbstractTargetedDAO<EntityMapping> { private final Random random = new Random ( System.currentTimeMillis () ); public EntityMappingDAO ( EntityManager entityManager ) { super ( entityManager, EntityMapping.class ); this.setEntityIdResolver ( new DbEntityIdResolver ( entityManager ) ); } /** * This calls {@link #storeMapping(String, String, String, String)} after having achieved the entity ID structure * via {@link #entityIdResolver}. */ public void storeMapping ( String entityId1, String entityId2 ) { EntityId eid1 = entityIdResolver.doall ( entityId1 ); EntityId eid2 = entityIdResolver.doall ( entityId2 ); storeMapping ( eid1.getServiceName (), eid1.getAcc (), eid2.getServiceName (), eid2.getAcc () ); } /** * Stores a mapping between two entities, i.e., a link between service1/acc1 and service2/acc2. Works as specified by * {@link EntityMappingManager#storeMappings(String...)}. * */ public void storeMapping ( String serviceName1, String accession1, String serviceName2, String accession2 ) { serviceName1 = StringUtils.trimToNull ( serviceName1 ); Validate.notNull ( serviceName1, "Cannot work with a null service name (first entity)" ); accession1 = StringUtils.trimToNull ( accession1 ); Validate.notNull ( accession1, "Cannot work with a null accession (first entity)" ); serviceName2 = StringUtils.trimToNull ( serviceName2 ); Validate.notNull ( serviceName2, "Cannot work with a null service name (2nd entity)" ); accession2 = StringUtils.trimToNull ( accession2 ); Validate.notNull ( accession2, "Cannot work with a null accession (2nd entity)" ); String bundle1 = this.findBundle ( serviceName1, accession1, true ); String bundle2 = this.findBundle ( serviceName2, accession2, true ); if ( bundle1 == null ) { if ( bundle2 == null ) // The mapping doesn't exist at all, create it // bundle1 = bundle2 = this.create ( serviceName2, accession2 ) ; // join the 1 side to bundle2 this.join ( serviceName1, accession1, bundle2 ); return; } // The same for the symmetric case // if ( bundle2 == null ) { // bundle1 is not null at this point, join the side 2 to bundle1 this.join ( serviceName2, accession2, bundle1 ); return; } // Bundles are both non null // // The mapping already exists if ( bundle1.equals ( bundle2 ) ) return; // They exists, but belongs to different bundles, so we need to merge them this.mergeBundles ( bundle1, bundle2 ); } /** * Assumes the array parameter contains pairs of entity IDs (see {@link #entityIdResolver}} and creates * a mapping for each pair (i.e., calls {@link #storeMapping(String, String)}). * * Throws an exception if the input is not a multiple of 2. * */ public void storeMappings ( String... entityIds ) { if ( entityIds == null || entityIds.length == 0 ) return; Validate.isTrue ( entityIds.length % 2 == 0, "Wrong no. of arguments for storeMappings, I expect a list of " + "(entity-id-1, entity-id-2) pairs" ); for ( int i = 0; i < entityIds.length; i++ ) storeMapping ( entityIds [ i ], entityIds [ ++ i ] ); } public void storeMappingBundle ( List<Entity> entities ) { if ( entities == null ) return; int nents = entities.size (); // Check if there is some entry already in // for ( int i = 0; i < nents; i++ ) { Entity ei = entities.get ( i ); String bundle = this.findBundle ( ei.getServiceName (), ei.getAccession (), true ); if ( bundle != null ) { // There is already a bundle with one of the input entities, so let's attach all of them to this for ( int j = 0; j < nents; j++ ) { if ( i == j ) continue; Entity ej = entities.get ( j ); String bundle1 = this.findBundle ( ej.getServiceName (), ej.getAccession (), true ); if ( bundle.equals ( bundle1 ) ) continue; if ( bundle1 == null ) this.join ( ej, bundle ); else this.moveBundle ( bundle1, bundle ); } return; } } // for i // It has not found any of the entries, so we need to create a new bundle that contains all of them. // String bundle = null; for ( int i = 0; i < nents; i++ ) { Entity e = entities.get ( i ); if ( bundle == null ) bundle = this.create ( e ); else this.join ( e, bundle ); } } public void storeMappingBundles ( EntityMappingSearchResult mappings ) { if ( mappings == null ) return; for ( Bundle bundle: mappings.getBundles () ) storeMappingBundle ( new ArrayList<> ( bundle.getEntities () ) ); } /** * Works like specified by {@link EntityMappingManager#storeMappingBundle(String...)}. * Uses {@link #entityIdResolver} to get the entity ID structure. * */ public void storeMappingBundle ( String... entityIds ) { if ( entityIds == null || entityIds.length == 0 ) return; // Check if there is some entry already in // for ( int i = 0; i < entityIds.length; i++ ) { EntityId eid = entityIdResolver.doall ( entityIds [ i ] ); String bundle = this.findBundle ( eid.getServiceName (), eid.getAcc (), true ); if ( bundle != null ) { // There is already a bundle with one of the input entities, so let's attach all of them to this for ( int j = 0; j < entityIds.length; j++ ) { if ( i == j ) continue; EntityId jeid = entityIdResolver.doall ( entityIds [ j ] ); String serviceNameJ = jeid.getServiceName (), accessionJ = jeid.getAcc (); String bundle1 = this.findBundle ( serviceNameJ, accessionJ, true ); if ( bundle.equals ( bundle1 ) ) continue; if ( bundle1 == null ) this.join ( serviceNameJ, accessionJ, bundle ); else this.moveBundle ( bundle1, bundle ); } // And then we're done return; } } // for i // It has not found any of the entries, so we need to create a new bundle that contains all of them. // String bundle = null; for ( int i = 0; i < entityIds.length; i++ ) { EntityId eid = entityIdResolver.doall ( entityIds [ i ] ); if ( bundle == null ) bundle = this.create ( eid.getServiceName (), eid.getAcc () ); else this.join ( eid.getServiceName (), eid.getAcc (), bundle ); } } /** * Uses {@link #entityIdResolver} to get the entity ID structure contained by the parameter * and then invokes {@link #deleteEntity(String, String)}. */ public boolean deleteEntity ( String entityId ) { EntityId eid = entityIdResolver.doall ( entityId ); return deleteEntity ( eid.getServiceName (), eid.getAcc () ); } /** * Deletes an entity from the database, i.e., it removes it from any equivalence/mapping relation it is involved in. * * @return true if the entity was in the DB and it was removed, false if that wasn't the case and the database was * left unchanged. */ public boolean deleteEntity ( String serviceName, String accession ) { // Invalid values serviceName = StringUtils.trimToNull ( serviceName ); if ( serviceName == null ) return false; accession = StringUtils.trimToNull ( accession ); if ( accession == null ) return false; String bundle = findBundle ( serviceName, accession ); if ( bundle == null ) return false; entityManager.createNativeQuery ( "DELETE FROM entity_mapping WHERE service_name = '" + serviceName + "' AND accession = '" + accession + "'" ).executeUpdate (); // If the bundle is left with 1 member only, must go away if ( ((Number) entityManager.createNativeQuery ( "SELECT COUNT( bundle ) AS ct FROM entity_mapping WHERE bundle = '" + bundle + "'" ).getSingleResult () ).longValue () == 1 ) entityManager.createNativeQuery ( "DELETE FROM entity_mapping WHERE bundle = '" + bundle + "'" ).executeUpdate (); return true; } /** * Assumes the input is a set of entity IDs and removes all the corresponding entities, * via {@link #deleteEntity(String)}. * * @return the number of entities that were deleted, i.e., the number of times {@link #deleteEntity(String, String)} * returned true. * */ public int deleteEntitites ( String... entityIds ) { if ( entityIds == null || entityIds.length == 0 ) return 0; int ct = 0; for ( int i = 0; i < entityIds.length; i++ ) ct += this.deleteEntity ( entityIds [ i ] ) ? 1 : 0; return ct; } /** * Uses {@link #entityIdResolver} to get the entity ID structure contained in the parameter, then invokes * {@link #deleteMappings(String, String)} with it. * */ public int deleteMappings ( String entityId ) { EntityId eid = entityIdResolver.doall ( entityId ); return deleteMappings ( eid.getServiceName (), eid.getAcc () ); } /** * Deletes all the mappings that involve the entity, i.e., the equivalence class it belongs to. * * @return the number of entities (including the parameter) that were in the same equivalence relationship and are * now deleted. Returns 0 if no such mapping exists. * */ public int deleteMappings ( String serviceName, String accession ) { // Invalid values serviceName = StringUtils.trimToNull ( serviceName ); if ( serviceName == null ) return 0; accession = StringUtils.trimToNull ( accession ); if ( accession == null ) return 0; String bundle = this.findBundle ( serviceName, accession, true ); return bundle == null ? 0 : this.deleteBundle ( bundle ); } /** * Assumes the input is a list of entity IDs and deletes all the relations such entities are involved * in, via {@link #deleteMappings(String)}. * * This call throws an exception if the input is not a multiple of 2. * * @returns the total number of entities related to the parameters (including the latter) that are now deleted. * */ public int deleteMappingsForAllEntitites ( String... entityIds ) { if ( entityIds == null || entityIds.length == 0 ) return 0; int ct = 0; for ( int i = 0; i < entityIds.length; i++ ) ct += this.deleteMappings ( entityIds [ i ] ); return ct; } /** * Uses {@link #entityIdResolver} to get the entity ID structure contained in the parameter, then invokes * {@link #findMappings(String, String)}. */ public List<String> findMappings ( String entityId, boolean mustBePublic ) { EntityId eid = entityIdResolver.doall ( entityId ); return findMappings ( eid.getServiceName (), eid.getAcc (), mustBePublic ); } /** Defaults to mustBePublic = true */ public List<String> findMappings ( String entityId ) { return findMappings ( entityId, true ); } /** * Finds all the entities that are related to the parameter. * * @return a possibly empty list of (serviceName, accession) pair. It <b>does include the parameter in the result</b>. * It returns an empty list if either parameter is empty. It never returns null. * */ public List<String> findMappings ( String serviceName, String accession, final boolean mustBePublic ) { final String serviceNameTrim = StringUtils.trimToNull ( serviceName ); final String accessionTrim = StringUtils.trimToNull ( accession ); final List<String> result = new ArrayList<String> (); if ( serviceName == null || accession == null ) return result; final String sql = mustBePublic ? "SELECT em1.service_name AS service_name, em1.accession AS accession\n" + "FROM entity_mapping em1, entity_mapping em2\n" + "WHERE em1.bundle = em2.bundle AND em2.service_name = ? AND em2.accession = ?\n" + // First of all the parameter entity must be public (or, transitively, one of its containers) "AND (\n" + " (em2.public_flag = 1 OR em2.public_flag IS NULL AND em2.release_date IS NOT NULL AND em2.release_date <= ? )\n" + " OR em2.public_flag IS NULL AND em2.release_date IS NULL AND em2.service_name IN (" + " SELECT name FROM service s WHERE ( s.public_flag = 1 OR s.public_flag IS NULL AND s.release_date IS NOT NULL AND s.release_date <= ? )\n" + " OR (s.public_flag IS NULL AND s.release_date IS NULL AND s.repository_name IN " + " (SELECT name FROM repository r WHERE r.public_flag = 1 OR r.public_flag IS NULL AND ( r.release_date IS NOT NULL AND r.release_date <= ? ) )" + " )\n" + " )\n" + ")\n" + // then, all the linked entities must be pub too "AND (\n" + " (em1.public_flag = 1 OR em1.public_flag IS NULL AND em1.release_date IS NOT NULL AND em1.release_date <= ? )\n" + " OR em1.public_flag IS NULL AND em1.release_date IS NULL AND em1.service_name IN (" + " SELECT name FROM service s WHERE ( s.public_flag = 1 OR s.public_flag IS NULL AND s.release_date IS NOT NULL AND s.release_date <= ? )\n" + " OR (s.public_flag IS NULL AND s.release_date IS NULL AND s.repository_name IN " + " (SELECT name FROM repository r WHERE r.public_flag = 1 OR r.public_flag IS NULL AND ( r.release_date IS NOT NULL AND r.release_date <= ? ) )" + " )\n" + " )\n" + ")" : "SELECT em1.service_name AS service_name, em1.accession AS accession FROM entity_mapping em1, entity_mapping em2\n" + " WHERE em1.bundle = em2.bundle AND em2.service_name = ? AND em2.accession = ?"; ((HibernateEntityManager)entityManager).getSession ().doWork ( new Work() { @Override public void execute ( Connection conn ) throws SQLException { PreparedStatement stmt = conn.prepareStatement ( sql ); stmt.setString ( 1, serviceNameTrim ); stmt.setString ( 2, accessionTrim ); if ( mustBePublic ) { java.sql.Date now = new java.sql.Date ( System.currentTimeMillis () ); for ( int i = 3; i <= 8; i++ ) stmt.setDate ( i, now ); } for ( ResultSet rs = stmt.executeQuery (); rs.next (); ) { String serviceNamei = rs.getString ( "service_name" ), accessioni = rs.getString ( "accession" ); result.add ( serviceNamei ); result.add ( accessioni ); } }} ); return result; } /** Defaults to mustBePublic = true */ public List<String> findMappings ( String serviceName, String accession ) { return findMappings ( serviceName, accession, true ); } /** * Uses {@link #entityIdResolver} to get the entity ID structure contained in the parameter, then invokes * {@link #findEntityMappings(String, String)}. * */ public List<EntityMapping> findEntityMappings ( String entityId, boolean mustBePublic ) { EntityId eid = entityIdResolver.doall ( entityId ); return findEntityMappings ( eid.getServiceName (), eid.getAcc (), mustBePublic ); } /** Defaults to mustBePublic = true */ public List<EntityMapping> findEntityMappings ( String entityId ) { return findEntityMappings ( entityId, true ); } /** * The same as {@link #findMappings(String, String)}, but returns a list of {@link EntityMapping}s from which * services can be fetched. Whether you need this or the version that returns strings, it depends on the type of * result needed, e.g., for raw results only the string-result version is faster, for complete results, this version * is faster and more practical. * */ @SuppressWarnings ( "unchecked" ) public List<EntityMapping> findEntityMappings ( String serviceName, String accession, boolean mustBePublic ) { serviceName = StringUtils.trimToNull ( serviceName ); accession = StringUtils.trimToNull ( accession ); if ( serviceName == null || accession == null ) return new ArrayList<EntityMapping> (); String queryName = mustBePublic ? "getPublicMappings" : "getAllMappings"; Query q = entityManager.createNamedQuery ( queryName, EntityMapping.class ) .setParameter ( "serviceName", serviceName ) .setParameter ( "accession", accession ); return q.getResultList (); } /** Defaults to mustBePublic = true */ public List<EntityMapping> findEntityMappings ( String serviceName, String accession ) { return findEntityMappings ( serviceName , accession, true ); } /** * Finds a single {@link EntityMapping}, ignoring all the involved mappings. */ @SuppressWarnings ( "unchecked" ) public EntityMapping findEntityMapping ( String serviceName, String accession, boolean mustBePublic ) { serviceName = StringUtils.trimToNull ( serviceName ); accession = StringUtils.trimToNull ( accession ); if ( serviceName == null || accession == null ) return null; String queryName = mustBePublic ? "findPublicEntityMapping" : "findEntityMapping"; Query q = entityManager.createNamedQuery ( queryName, EntityMapping.class ) .setParameter ( "serviceName", serviceName ) .setParameter ( "accession", accession ); List<EntityMapping> result = q.getResultList (); return result == null || result.isEmpty () ? null : result.get ( 0 ); } /** Defaults to mustBePublic = true */ public EntityMapping findEntityMapping ( String serviceName, String accession ) { return findEntityMapping ( serviceName, accession, true ); } /** * Works like {@link #findEntityMappings(String, String, boolean)}, splits the ID into its chunks. */ public EntityMapping findEntityMapping ( String entityId, boolean mustBePublic ) { EntityId eid = entityIdResolver.doall ( entityId ); return findEntityMapping ( eid.getServiceName (), eid.getAcc (), mustBePublic ); } /** Defaults to mustBePublic = true */ public EntityMapping findEntityMapping ( String entityId ) { return findEntityMapping ( entityId, true ); } public List<EntityMapping> findMappingsForTarget ( String targetServiceName, String entityId, boolean mustBePublic) { EntityId eid = entityIdResolver.doall ( entityId ); return findMappingsForTarget ( targetServiceName, eid.getServiceName (), eid.getAcc (), mustBePublic ); } /** Defaults to mustBePublic = true */ public List<EntityMapping> findMappingsForTarget ( String targetServiceName, String entityId ) { return findMappingsForTarget ( targetServiceName, entityId, true ); } /** * Returns the entity IDs of those entities that are equivalent to the input entity and belong to the * specified target service. It returns a list, cause nothing forbids to map an entity into multiple ones * onto the same target service. * */ @SuppressWarnings ( "unchecked" ) public List<EntityMapping> findMappingsForTarget ( String targetServiceName, String serviceName, String accession, boolean mustBePublic ) { serviceName = StringUtils.trimToNull ( serviceName ); accession = StringUtils.trimToNull ( accession ); targetServiceName = StringUtils.trimToNull ( targetServiceName ); if ( serviceName == null || accession == null || targetServiceName == null ) return new ArrayList<EntityMapping> (); String queryName = mustBePublic ? "findPublicMappingsForTarget" : "findMappingsForTarget"; Query q = entityManager.createNamedQuery ( queryName, EntityMapping.class ) .setParameter ( "serviceName", serviceName ) .setParameter ( "accession", accession ) .setParameter ( "targetServiceName", targetServiceName ); return q.getResultList (); } /** Defaults to mustBePublic = true */ public List<EntityMapping> findMappingsForTarget ( String targetServiceName, String serviceName, String accession ) { return findMappingsForTarget ( targetServiceName, serviceName, accession, true ); } /** * Creates a new {@link EntityMapping}, assigning a new bundle. This is a wrapper of {@link #create(String, String, String)}, * with bundle = null. * * @return the new bundle ID. * */ private String create ( String serviceName, String accession ) { return create ( serviceName, accession, null ); } /** * Joins an entity to an existing bundle, i.e., creates a new {@link EntityMapping} with the parameters. This is * a wrapper to {@link #create(String, String, String)} with an exception in the case that bundle == null. * */ private String join ( String serviceName, String accession, String bundle ) { if ( bundle == null ) throw new RuntimeException ( "Cannot work with an empty bundle ID" ); return create ( serviceName, accession, bundle ); } private String join ( Entity e, String bundle ) { if ( bundle == null ) throw new RuntimeException ( "Cannot work with an empty bundle ID" ); return create ( e, bundle ); } /** * Creates a new {@link EntityMapping}, by first creating a new bundle (via {@link #createNewBundleId()}) * if the corresponding parameter is null. * * @return the newly created bundle or the bundle parameter if this is not null. */ private String create ( String serviceName, String accession, String bundle ) { if ( bundle == null ) bundle = createNewBundleId (); Query q = entityManager.createNativeQuery ( "INSERT INTO entity_mapping (service_name, accession, bundle) VALUES ( :serviceName, :acc, :bundle )" ); q.setParameter ( "serviceName", serviceName ); q.setParameter ( "acc", accession ); q.setParameter ( "bundle", bundle ); // TODO: check the return value q.executeUpdate (); return bundle; } private String create ( Entity e, String bundle ) { if ( bundle == null ) bundle = createNewBundleId (); Date relDate = e.getReleaseDate (); Boolean pubFlag = e.getPublicFlag (); Query q = entityManager.createNativeQuery ( String.format ( "INSERT INTO entity_mapping (service_name, accession, bundle, %s %s) " + "VALUES ( :serviceName, :acc, :bundle, %s %s)", (relDate == null ? "" : "release_date," ), (pubFlag == null ? "" : "public_flag" ), (relDate == null ? "" : ":release_date," ), (pubFlag == null ? "" : ":public_flag" ) )); q.setParameter ( "serviceName", e.getServiceName () ); q.setParameter ( "acc", e.getAccession () ); q.setParameter ( "bundle", bundle ); if ( relDate != null ) q.setParameter ( "release_date", e.getReleaseDate () ); if ( pubFlag != null ) q.setParameter ( "public_flag", pubFlag ? 1 : 0 ); // TODO: check the return value q.executeUpdate (); return bundle; } private String create ( Entity e ) { return create ( e, null ); } /** * Merges two bundles, i.e., takes all the {@link EntityMapping} having one of the two bundle IDs and replace their * bundle ID with the value of the other parameter. Due to optimisation needs, the method selects which bundle to * change randomly (so you cannot know which one is preserved after the operation). * * @returns the new unique bundle that there is now in the DB (i.e., either bundle1 or bundle2, the other bundle * doesn't exist anymore and was merged with the one that is returned). * * This low-level operation is used to store possibly new mappings. It is essentially a random swap of the * parameters, followed by {@link #moveBundle(String, String)}. * */ private String mergeBundles ( String bundle1, String bundle2 ) { bundle1 = StringUtils.trimToNull ( bundle1 ); bundle2 = StringUtils.trimToNull ( bundle2 ); // Bundles have roughly the same size, let's choose which one to update randomly, just to make the no. of times // that the smaller bundle is chosen uniform. // if ( random.nextBoolean () ) { String tmp = bundle1; bundle1 = bundle2; bundle2 = tmp; } moveBundle ( bundle1, bundle2 ); return bundle2; } /** * Moves srcBundle to destBundle, i.e., replaces all the {@link EntityMapping}(s) in srcBundle with destBundle. * This low-level operation is used to store possibly new mappings. * */ private void moveBundle ( String srcBundle, String destBundle ) { srcBundle = StringUtils.trimToNull ( srcBundle ); destBundle = StringUtils.trimToNull ( destBundle ); Query q = entityManager.createNativeQuery ( "UPDATE entity_mapping SET bundle = '" + destBundle + "' WHERE bundle = '" + srcBundle + "'" ); // TODO: check > 0 q.executeUpdate (); } /** * Wraps isForUpdate = false */ private String findBundle ( String serviceName, String accession ) { return findBundle ( serviceName, accession, false ); } /** * @return the ID of the bundle that the parameter belongs to. null if there is no such bundle. * isForUpdate = true is used in DAO methods that need to perform changes on the {@link EntityMapping} * * table. Setting the flag cause tells this method to use SQL-locking mechanisms, such as FOR UPDATE, * which allows the DB to know that the table/records involved in the query are being updated within * a transaction. * */ private String findBundle ( String serviceName, String accession, boolean isForUpdate ) { String sql = "SELECT bundle FROM entity_mapping\n" + " WHERE service_name = :serviceName AND accession = :accession"; if ( isForUpdate ) sql += " FOR UPDATE"; Query q = entityManager.createNativeQuery ( sql ) .setParameter ( "serviceName", serviceName ) .setParameter ( "accession", accession ); // we cannot use this together with FOR UPDATE, since the extra syntax (LIMIT, rownum <= 1) is placed // after FOR UPDATE. We haven't notice a significant performance change anyway, and we're keeping this restriction // for read-only queries just in case. // if ( !isForUpdate ) q.setMaxResults ( 1 ); // We've seen this problem (http://stackoverflow.com/questions/4873201/hibernate-native-query-char3-column) q.unwrap ( SQLQuery.class ).addScalar ( "bundle", StringType.INSTANCE ); @SuppressWarnings ( "unchecked" ) List<String> results = q.getResultList (); return results.isEmpty () ? null : results.iterator ().next (); } /** * Deletes a bundle by ID, i.e., removes all the {@link EntityMapping} that have the parameter as bundle ID. * */ private int deleteBundle ( String bundle ) { return entityManager.createNativeQuery ( "DELETE FROM entity_mapping WHERE bundle = :bundle" ) .setParameter ( "bundle", bundle ) .executeUpdate (); } public int dump ( OutputStream out, Integer offset, Integer limit, double randomQuota ) { int result = 0; Random rnd = new Random (); Session session = (Session) this.entityManager.getDelegate (); String sql = "SELECT bundle, service_name, accession, release_date, public_flag FROM entity_mapping ORDER BY bundle"; SQLQuery qry = session.createSQLQuery ( sql ); // TODO: needs hibernate.jdbc.batch_size? qry .setReadOnly ( true ) .setFetchSize ( 1000 ) .setCacheable ( false ) .setCacheMode ( CacheMode.IGNORE ); if ( offset != null && offset >= 0 ) qry.setFirstResult ( offset ); if ( limit != null && offset < Integer.MAX_VALUE ) qry.setMaxResults ( limit ); List<String> entityIds = new ArrayList<> (); List<Date> relDates = new ArrayList<> (); List<Boolean> pubFlags = new ArrayList<> (); String prevBundle = null; for ( ScrollableResults rs = qry.scroll ( ScrollMode.FORWARD_ONLY ); rs.next (); ) { result++; String bundle = (String) rs.get ( 0 ); if ( prevBundle == null ) prevBundle = bundle; String serviceName = (String) rs.get ( 1 ), acc = (String) rs.get ( 2 ); String entityId = serviceName + ":" + acc; if ( !bundle.equals ( prevBundle ) ) { // Jump a random amount of data if ( rnd.nextDouble () >= randomQuota ) continue; // Now dump what we got so far // EntityMappingSearchResult maps = new EntityMappingSearchResult ( true ); List<EntityMapping> ents = new LinkedList<EntityMapping> (); int i = 0; for ( String thisEntityId: entityIds ) { EntityId eid = entityIdResolver.doall ( thisEntityId ); Service service = new Service ( eid.getServiceName () ); EntityMapping ent = new EntityMapping ( service, eid.getAcc (), prevBundle ); ent.setReleaseDate ( relDates.get ( i ) ); ent.setPublicFlag ( pubFlags.get ( i ) ); ents.add ( ent ); i++; } maps.addAllEntityMappings ( ents ); JAXBUtils.marshal ( maps.getBundles ().iterator ().next (), Bundle.class, out, Marshaller.JAXB_FRAGMENT, true ); // reset all the accumulators entityIds = new ArrayList<> (); relDates = new ArrayList<> (); pubFlags = new ArrayList<> (); // Start with a new bundle prevBundle = bundle; } // if bundle changed entityIds.add ( entityId ); relDates.add ( (Date) rs.get ( 3 ) ); BigDecimal pubFlag = ((BigDecimal) rs.get ( 4 )); pubFlags.add ( pubFlag == null ? null : pubFlag.intValue () == 1 ); } // for rs return result; } // dump () /** * Creates a new bundle ID to create a new bundle with the parameter. This is supposed to be used when a new bundle * is being inserted in the DB, i.e., because the parameter entity is not in any other stored bundle yet. * * You should assume the method returns an opaque string which of value depends 1-1 from the parameter. * * At the moment it uses UUIDs, encoded in BASE64. This generates some overhead in both space and time, but we * prefer to deal with string IDs, rather than not-so-portable byte arrays. * */ private static String createNewBundleId () { return IdUtils.createCompactUUID (); } public void setEntityManager ( EntityManager entityManager ) { this.entityManager = entityManager; } }