web-dev-qa-db-fra.com

Java Iterator soutenu par un ResultSet

J'ai une classe qui implémente Iterator avec un ResultSet en tant que membre de données. En gros, la classe ressemble à ceci:

public class A implements Iterator{
    private ResultSet entities;
    ...
    public Object next(){
        entities.next();
        return new Entity(entities.getString...etc....)
    }

    public boolean hasNext(){
        //what to do?
    }
    ...
}

Comment puis-je vérifier si le ResultSet a une autre ligne afin que je puisse créer une méthode hasNext valide puisque ResultSet n'a pas défini de hasNext? Je pensais faire une requête SELECT COUNT(*) FROM... pour obtenir le nombre et gérer ce nombre pour voir s'il y avait une autre ligne, mais j'aimerais éviter cela.

27
Jordan Messina

C'est une mauvaise idée. Cette approche nécessite que la connexion soit ouverte tout le temps jusqu'à la lecture de la dernière ligne. En dehors de la couche DAO, vous ne savez jamais quand cela se produira. Vous semblez également laisser le jeu de résultats ouvert et risquer des fuites de ressources et le blocage de l'application la connexion a expiré. Vous ne voulez pas avoir ça. 

La pratique habituelle de JDBC consiste à acquérir Connection, Statement et ResultSet dans la portée de shortest possible. La pratique habituelle est également de mapper plusieurs lignes dans un List ou peut-être un Map et devinez quoi, ils do ont un Iterator.

public List<Data> list() throws SQLException {
    List<Data> list = new ArrayList<Data>();

    try (
        Connection connection = database.getConnection();
        Statement statement = connection.createStatement("SELECT id, name, value FROM data");
        ResultSet resultSet = statement.executeQuery();
    ) {
        while (resultSet.next()) {
            list.add(map(resultSet));
        }
    }

    return list;
}

private Data map(ResultSet resultSet) throws SQLException {
    Data data = new Data(); 
    data.setId(resultSet.getLong("id"));
    data.setName(resultSet.getString("name"));
    data.setValue(resultSet.getInteger("value"));
    return data;
}

Et utilisez-le comme ci-dessous:

List<Data> list = dataDAO.list(); 
int count = list.size(); // Easy as that.
Iterator<Data> iterator = list.iterator(); // There is your Iterator.

Ne transmettez pas des ressources DB coûteuses en dehors de la couche DAO comme vous le souhaitiez initialement. Pour des exemples plus élémentaires de pratiques JDBC normales et du modèle DAO, vous pouvez trouver cet article utile.

35
BalusC

Vous pouvez vous en sortir en effectuant une recherche dans la fonction hasNext() et en vous rappelant que vous avez effectué une recherche afin d'éviter de consommer trop d'enregistrements, comme par exemple:

public class A implements Iterator{
    private ResultSet entities;
    private boolean didNext = false;
    private boolean hasNext = false;
    ...
    public Object next(){
        if (!didNext) {
            entities.next();
        }
        didNext = false;
        return new Entity(entities.getString...etc....)
    }

    public boolean hasNext(){
        if (!didNext) {
            hasNext = entities.next();
            didNext = true;
        }
        return hasNext;
    }
    ...
}
38
rsp

ResultSet a une méthode 'isLast ()' qui pourrait répondre à vos besoins. Le JavaDoc indique qu'il est assez coûteux, car il doit être lu à l'avance. Il y a de fortes chances qu'il mette en cache la valeur d'anticipation comme les autres suggèrent d'essayer.

5
Charlie Brown
public class A implements Iterator<Entity>
{
    private final ResultSet entities;

    // Not required if ResultSet.isLast() is supported
    private boolean hasNextChecked, hasNext;

    . . .

    public boolean hasNext()
    {
        if (hasNextChecked)
           return hasNext;
        hasNext = entities.next();
        hasNextChecked = true;
        return hasNext;

        // You may also use !ResultSet.isLast()
        // but support for this method is optional 
    }

    public Entity next()
    {
        if (!hasNext())
           throw new NoSuchElementException();

        Entity entity = new Entity(entities.getString...etc....)

        // Not required if ResultSet.isLast() is supported
        hasNextChecked = false;

        return entity;
    }
}
3
Venkata Raju

Ce n'est pas vraiment une mauvaise idée dans les cas où vous en avez besoin, c'est simplement que vous n'en avez souvent pas besoin.

Si vous avez besoin de faire quelque chose comme, par exemple, diffuser toute votre base de données ... vous pouvez pré-extraire la ligne suivante - si l'extraction échoue, votre hasNext est false.

Voici ce que j'ai utilisé:

/**
 * @author Ian Pojman <[email protected]>
 */
public abstract class LookaheadIterator<T> implements Iterator<T> {
    /** The predetermined "next" object retrieved from the wrapped iterator, can be null. */
    protected T next;

    /**
     * Implement the hasNext policy of this iterator.
     * Returns true of the getNext() policy returns a new item.
     */
    public boolean hasNext()
    {
        if (next != null)
        {
            return true;
        }

        // we havent done it already, so go find the next thing...
        if (!doesHaveNext())
        {
            return false;
        }

        return getNext();
    }

    /** by default we can return true, since our logic does not rely on hasNext() - it prefetches the next */
    protected boolean doesHaveNext() {
        return true;
    }

    /**
     * Fetch the next item
     * @return false if the next item is null. 
     */
    protected boolean getNext()
    {
        next = loadNext();

        return next!=null;
    }

    /**
     * Subclasses implement the 'get next item' functionality by implementing this method. Implementations return null when they have no more.
     * @return Null if there is no next.
     */
    protected abstract T loadNext();

    /**
     * Return the next item from the wrapped iterator.
     */
    public T next()
    {
        if (!hasNext())
        {
            throw new NoSuchElementException();
        }

        T result = next;

        next = null;

        return result;
    }

    /**
     * Not implemented.
     * @throws UnsupportedOperationException
     */
    public void remove()
    {
        throw new UnsupportedOperationException();
    }
}

puis:

    this.lookaheadIterator = new LookaheadIterator<T>() {
        @Override
        protected T loadNext() {
            try {
                if (!resultSet.next()) {
                    return null;
                }

                // process your result set - I use a Spring JDBC RowMapper
                return rowMapper.mapRow(resultSet, resultSet.getRow());
            } catch (SQLException e) {
                throw new IllegalStateException("Error reading from database", e);
            }
        }
    };
}
3
Ian Pojman

Une option est le ResultSetIterator du projet Apache DBUtils. 

BalusC souligne à juste titre les différentes préoccupations qui se posent à cet égard. Vous devez être très prudent pour gérer correctement le cycle de vie de la connexion/du jeu de résultats. Heureusement, le projet DBUtils a également solutions pour travailler en toute sécurité avec les ensembles de résultats. 

Si la solution de BalusC n’est pas pratique pour vous (par exemple, vous traitez des jeux de données volumineux qui ne peuvent pas tous tenir dans la mémoire), vous voudrez peut-être tenter votre chance.

3
NobodyMan

Vous pouvez essayer ce qui suit:

public class A implements Iterator {
    private ResultSet entities;
    private Entity nextEntity;
    ...
    public Object next() {
        Entity tempEntity;
        if ( !nextEntity ) {
            entities.next();
            tempEntity = new Entity( entities.getString...etc....)
        } else {
            tempEntity = nextEntity;
        }

        entities.next();
        nextEntity = new Entity( entities.getString...ext....)

        return tempEntity;
    }

    public boolean hasNext() {
        return nextEntity ? true : false;
    }
}

Ce code met en cache l'entité suivante et hasNext () renvoie true si l'entité mise en cache est valide, sinon il renvoie false.

2
ComSubVie

Vous pouvez faire certaines choses en fonction de ce que vous voulez dans votre classe A. Si le principal cas d'utilisation consiste à analyser chaque résultat, il est peut-être préférable de précharger tous les objets Entity et de jeter le ResultSet.

Si toutefois vous ne voulez pas faire cela, vous pouvez utiliser les méthodes next () et previous () de ResultSet.

public boolean hasNext(){
       boolean next = entities.next();

       if(next) {

           //reset the cursor back to its previous position
           entities.previous();
       }
}

Vous devez vous assurer de ne pas lire le ResultSet, mais si votre classe Entity est un bon POJO (ou au moins, correctement déconnecté de ResultSet, alors cela devrait être une bonne approche.

2
Dunderklumpen

Je suis d'accord avec BalusC. Permettre à un itérateur d'échapper à votre méthode DAO rendra difficile la fermeture des ressources de connexion. Vous serez obligé de connaître le cycle de vie de la connexion en dehors de votre DAO, ce qui entraîne un code encombrant et des fuites de connexion potentielles.

Cependant, un choix que j'ai utilisé consiste à passer un type Function ou Procedure à la méthode DAO. En gros, transmettez une sorte d’interface de rappel qui prend chaque ligne de votre jeu de résultats.

Par exemple, peut-être quelque chose comme ceci:

public class MyDao {

    public void iterateResults(Procedure<ResultSet> proc, Object... params)
           throws Exception {

        Connection c = getConnection();
        try {
            Statement s = c.createStatement(query);
            ResultSet rs = s.executeQuery();
            while (rs.next()) {
                proc.execute(rs);
            }

        } finally {
            // close other resources too
            c.close();
        }
    }

}


public interface Procedure<T> {
   void execute(T t) throws Exception;
}


public class ResultSetOutputStreamProcedure implements Procedure<ResultSet> {
    private final OutputStream outputStream;
    public ResultSetOutputStreamProcedure(OutputStream outputStream) {
        this.outputStream = outputStream;
    }

    @Override
    public void execute(ResultSet rs) throws SQLException {
        MyBean bean = getMyBeanFromResultSet(rs);
        writeMyBeanToOutputStream(bean);
    }    
}

De cette manière, vous conservez les ressources de connexion à la base de données dans votre DAO, ce qui est approprié. Cependant, vous n'êtes pas obligé de remplir une collection si la mémoire est un sujet de préoccupation.

J'espère que cela t'aides.

2
taftster

Vous pouvez utiliser ResultSetIterator , il suffit de placer votre ResultSet dans le constructeur.

ResultSet rs = ...    
ResultSetIterator = new ResultSetIterator(rs); 
1
B. Broto

Les itérateurs sont problématiques pour parcourir les ResultSets pour les raisons mentionnées ci-dessus, mais ils ressemblent au comportement avec toute la sémantique requise pour la gestion des erreurs et la fermeture des ressources est disponible avec les séquences réactives (Observables) dans RxJava . Les observables sont comme des itérateurs mais incluent les notions d'abonnement, de leur annulation et du traitement des erreurs. 

Le projet rxjava-jdbc comporte des implémentations d'Observables pour les opérations jdbc, notamment des traversées de ResultSets avec une fermeture appropriée des ressources, un traitement des erreurs et la possibilité d'annuler la traversée selon les besoins (désabonnement).

1
Dave Moten

entités.next retourne false s'il n'y a plus de lignes, vous pouvez donc obtenir cette valeur de retour et définir une variable membre pour suivre l'état de hasNext (). 

Mais pour que cela fonctionne, vous devez également avoir une sorte de méthode init qui lit la première entité et la mette en cache dans la classe. Ensuite, lors de l'appel suivant, vous devrez renvoyer la valeur précédemment mise en cache et mettre en cache la valeur suivante, etc.

1
Justin Ethier

Voici mon itérateur qui encapsule un ResultSet. Les lignes sont retournées sous la forme d'une carte. J'espère que vous le trouverez utile. La stratégie est que j'apporte toujours un élément à l'avance.

public class ResultSetIterator implements Iterator<Map<String,Object>> {

    private ResultSet result;
    private ResultSetMetaData meta;
    private boolean hasNext;

    public ResultSetIterator( ResultSet result ) throws SQLException {
        this.result = result;
        meta = result.getMetaData();
        hasNext = result.next();
    }

    @Override
    public boolean hasNext() {
        return hasNext;
    }

    @Override
    public Map<String, Object> next() {
        if (! hasNext) {
            throw new NoSuchElementException();
        }
        try {
            Map<String,Object> next = new LinkedHashMap<>();
            for (int i = 1; i <= meta.getColumnCount(); i++) {
                String column = meta.getColumnName(i);
                Object value = result.getObject(i);
                next.put(column,value);
            }
            hasNext = result.next();
            return next;
        }
        catch (SQLException ex) {
            throw new RuntimeException(ex);
        }
    }
}
1
Giannis

Je pense que l'on comprend assez pourquoi il est vraiment déplorable d'utiliser ResultSet dans un Iterator (bref, ResultSet maintient une connexion active à la base de données et ne le ferme pas aussitôt que possible, ce qui peut entraîner des problèmes).

Mais dans une situation différente, si vous obtenez ResultSet (rs) et que vous allez parcourir les éléments, vous souhaitez également faire quelque chose avant l'itération, comme ceci:

if (rs.hasNext()) { //This method doesn't exist
    //do something ONCE, *IF* there are elements in the RS
}
while (rs.next()) {
    //do something repeatedly for each element
}

Vous pouvez obtenir le même effet en l’écrivant comme ceci:

if (rs.next()) {
    //do something ONCE, *IF* there are elements in the RS
    do {
        //do something repeatedly for each element
    } while (rs.next());
}
0
ADTC

Cela peut être fait comme ça:

public boolean hasNext() {
    ...
    return !entities.isLast();
    ...
}
0
Art

Vous attendez-vous à ce que la plupart des données de votre jeu de résultats soient réellement utilisées? Si c'est le cas, pré-cachez-le. C'est assez trivial en utilisant par exemple Spring

  List<Map<String,Object>> rows = jdbcTemplate.queryForList(sql);
  return rows.iterator();

Ajustez à votre goût.

0
Dan