Skip to content

6 MongoDB en Java

Introducción

Para trabajar en Java con MongoDB necesitamos descargar el driver desde la URL de MongoDB https://mongodb.github.io/mongo-java-driver

BSON

BSON es un formato de serialización binaria, se utiliza para almacenar documentos y hacer llamadas a procedimientos en MongoDB. La especificación BSON se encuentra en bsonspec.org. BSON soporta los siguientes tipos de datos como valores en los documentos, cada tipo de dato tiene un número y un alias que se pueden utilizar con el operador $type para consultar los documentos por tipo BSON. Algunos de los tipos BSON son los siguientes:

Tipo Número Alías
Double 1 "double"
String 2 "string"
Object 3 "object"
Array 4 "array"
Binary data 5 "binData"
ObjectId 7 "objectId"
Boolean 8 "bool"
Date 9 "date"
Null 10 "null"
Symbol 14 "symbol"
Timestamp 17 "timestamp"

Al comparar los valores de los diferentes tipos BSON, MongoDB utiliza el siguiente orden de comparación, de menor a mayor: Null, Numbers(int, long, double), Symbol, String, Object, Array, BinData, ObjectId, Boolean, Date, Timestamp

Conexión a la BD

Para conectarnos a la base de datos creamos una instancia de MongoClient, por defecto crea una conexión con la base de datos local, y escucha por el puerto 27017. Todos los métodos relacionados con operaciones CRUD (C*reate, R*ead, U*pdate and *D*elete) en Java se accede a través de la interfaz ***MongoCollection. Las instancias de MongoCollection se pueden obtener a partir de la interfaz MongoClient por medio de una MongoDatabase

MongoClient client = MongoClients.create(URI);
MongoDatabase db = client.getDatabase(DB);
MongoCollection<Document> collection = db.getCollection(COLLECTION);
client.close(); // Close connection

MongoCollection es una interfaz genérica: el parámetro de tipo TDocument es la clase que los clientes utilizan para insertar o modificar los documentos de una colección, y es el tipo predeterminado para devolver búsqueda y agregados. El método de un solo argumento getCollection devuelve una instancia de MongoCollection<Document>, y así podemos trabajar con instancias de la clase de documento.

Visualizar los datos de una colección

Los datos de una colección se pueden cargar en una lista utilizando el método find().into() de la siguiente manera:

List<Document> query = collection.find().into(new ArrayList<>());

for (Document document : query) {
    System.out.println(document);
}

Con el método find() se realiza la consulta, mientras que con el método into pasamos dicha consulta a la colección de datos que deseemos.

También podemos recuperar los valor de los campos del documento, utilizando los métodos get del objeto Document, que recibe como parámetro el nombre de la clave. Si se sabe el tipo de dato de la clave elegiremos el método correspondiente, y si no utilizamos get() que devuelve un objeto. Primero cargamos el elemento de la lista en un Document. Si la clave no existe en el documento visualizará null

 for (Document document : query) {
    System.out.printf("Nombre: %s ", document.getString("nombre"));
    System.out.printf("Edad: %d ", document.getInteger("edad", 0));
    System.out.printf("On: %b\n", document.getBoolean("on", false));
}

Insertar documentos

Para insertar documentos, creamos un objeto Document, con el método put asignamos los pares clave-valor, donde el primer parámetro es el nombre del campo o la clave, y el segundo el valor. Y con el método insertOne se inserta en la colección.

Document doc = new Document();
doc.put("nombre", "María");
doc.put("edad", 18);
collection.insertOne(doc);

También se puede insertar documentos utilizando el método append de Document. Por ejemplo, se va a insertar el siguiente documento, se crea en curso un nuevo documento con dos pares clave-valor:

Document doc = new Document("nombre", "Marcos")
        .append("edad", 18)
        .append("cursos", new Document()
                .append("curso1", "1DAM")
                .append("curso2", "2DAM")
        );

collection.insertOne(doc);

Dicho documento se visualizaría de la siguiente forma:

{
    "Nombre": "Pedro",
    "teléfono": 12345,
    "curso": {
        "curso1": "1DAM",
        "curso2": "2DAM",
    }
}

A la hora de visualizar el curso utilizaremos el método get() en lugar de getString().

Se puede insertar en la base de datos una lista de documentos en una colección utilizando el método insertMany

Si se desea saber los documentos de una colección se puede utilizar el método countDocuments:

System.out.println("Nº documentos: " + collection.countDocuments());

Iterable

El método find() devuelve un cursor de una instancia FindIterable. Podemos utilizar el método iterator() para recorrer el cursor.

MongoCursor<Document> cursor = collection.find().iterator();
Document document;

while(cursor.hasNext()){
    document = cursor.next();
    System.out.println(document.toJson());
}
cursor.close();

Si solo se desea obtener el primer documento utilizamos el método first():

Document first = collection.find().first();
System.out.println(first.toJson());

Utilizar filtros

El método find() admite la utilización de filtros. Para utilizar los métodos de la clase Filters hacemos un import static de la clase Filters de la siguiente manera:

import static com.mongodb.client.model.Filters.*;

Podemos usar sus diferentes métodos como, eq para poder localizar los documentos cuyo elementos sean igual a lo indicado

Document document1 = collection.find(eq("nombre", "prueba")).first();

if(document1 == null) {
    System.out.println("Documento con nombre 'prueba' no encontrado");
} else {
    System.out.println(document1);
}

Si el filtro devuelve varios documentos los recuperamos con un cursor, o bien con una lista.

Si se desea extraer los objetos BSON de un documento, utilizaremos los filtros:

System.out.println("-------------------------OBJETOS BSON---------------------------");
MongoCursor<Document> cursor2 = collection.find().iterator();
Document document2;

while(cursor2.hasNext()){
    document2 = cursor2.next();
    Bson id = eq("_id", document2.get("_id"));
    Bson nombre = eq("nombre", document2.get("nombre"));
    Bson curso = eq("curso", document2.get("curso"));
    System.out.println("Id: " + id + ". Nombre: " + nombre + ". Curso: " + curso);
}

cursor2.close();

Ordenar resultados

Para ordenar el resultado de una consulta importamos los métodos de la clase Sorts:

import static com.mongodb.client.model.Sorts.*;

De esta forma podemos obtener todos los documentos que tenga 2ºDAM, ordenados de forma descendiente por el nombre:

collection.find(eq("curso", "2DAM")).sort(descending("nombre"));

Utilizar proyecciones

A veces no se necesitan todos los datos contenidos en un documento, se pueden utilizar proyecciones para cambiar las salidas. Se necesitan importar los métodos de la clase Projection, estos métodos devuelven un tipo BSON, que podrá ser utilizado en otro método. El import debe ser el siguiente:

import static com.mongodb.client.model.Projections.*;

De esta manera, solo vamos a obtener el nombre y la edad de cada persona:

MongoCursor<Document> cursor3 = collection.find()
        .sort(ascending("nombre"))
        .projection(Projections.include("nombre", "edad"))
        .iterator();

while (cursor3.hasNext()){
    System.out.println(cursor3.next().toJson());
}

cursor3.close();

El método include se utiliza para indicar los elementos que se desea visualizar, con el método exclude, se indica los elementos que no se desea visualizar.

Utilizar agregaciones

Para utilizar los agregados se necesitan importar los métodos de la clase Aggregates. Cada método devuelve una instancia del tipo BSON, que a su vez se puede pasar al método de agregado de MongoCollection. El import debe ser:

import static com.mongodb.client.model.Aggregates.*;

De esta manera:

MongoCursor<Document> cursor4 = collection.aggregate(
        List.of(match(eq("curso", "1DAM")))
).iterator();

while (cursor4.hasNext()){
    System.out.println(cursor4.next().toJson());
}

cursor4.close();

Actualizar documentos

Para actualizar las propiedad de un documento, podemos usar los método updateOne y updateMany, para actualizar uno o varios documentos. Para poder actualizar un documento será necesario:

  • el filtro de consulta para localizar el/los elementos a actualizar, que puede ser un objeto Bson de la clase Filters o puede ser un nuevo Documento.
  • la actualización de las propiedades con los métodos de la clase de utilidad Updates, por ejemplo, el método set(prop, value) indica un nuevo valor para dicha propiedad (o añade la propiedad si no existe), inc(prop, value) incrementa el valor en la propiedad indicada, o unset(prop), elimina la propiedad del documento. Se puede combinar varias actualizaciones haciendo uso del método combine.
  • opciones de actualización, como por ejemplo indicar que se inserte el documento en caso de que no exista, con el método upsert(true) de la clase UpdateOptions.

La actualización devuelve un objeto de tipo UpdateResult en el que podemos comprobar cuando documentos han sido modificados (getModifiedCount()) y cuantos elementos han sido seleccionados (getMatchedCount()).

collection.updateOne(eq("nombre", "Ana"), set("nota", 5));

UpdateResult updateResult = collection.updateMany(eq("curso", "1DAM"), inc("nota", 1));

System.out.println("Se han modificado: " + updateResult.getModifiedCount());
System.out.println("Se han seleccionado: " + updateResult.getMatchedCount());

También existe el método findOneAndUpdate(), que localiza el documento a actualizar y lo actualiza y en lugar de devolverle un UpdateResult devuelve el documento de la forma indicada en las opciones de actualización (de la clase FindOneAnUpdateOptions). Con el método returnDocument(value) podemos indicar que retorne el documento antes de actualizar (ReturnDocument.BEFORE) o después de actualizar (ReturnDocument.AFTER)

Reemplazar documentos

Con replaceOne se puede reemplazar un documento, indicándole la consulta de filtrado, el nuevo documento a reemplazar y las opciones de reemplazado (como por ejemplo upsert de la clase ReplaceOptions).

Bson query = eq("title", "Music of the Heart");
Document replaceDocument = new Document().
        append("title", "50 Violins").
        append("fullplot", " A dramatization of the true story of Roberta Guaspari who co-founded the Opus 118 Harlem School of Music");
ReplaceOptions opts = new ReplaceOptions().upsert(true);
UpdateResult result = collection.replaceOne(query, replaceDocument, opts);

La diferencia entre reemplazar y actualizar, es que actualizar me permite realizar actualizaciones de propiedades específicas, mientras que reemplazar, reemplaza (o actualiza) el documento completo.

También existe el método findOneAndReplace(), que localiza el documento a reemplazar y lo reemplaza y en lugar de devolverle un UpdateResult devuelve el documento de la forma indicada en las opciones de reemplazo (de la clase FindOneAndReplaceOptions). Con el método returnDocument(value) podemos indicar que retorne el documento antes de reemplazar (ReturnDocument.BEFORE) o después de reemplazar (ReturnDocument.AFTER)

Borrar un documento de la colección

Para eliminar un documento se usará deleteOne y para borrar varios deleteMany. Ambos devuelve un DeleteResult

DeleteResult result1 = collection.deleteMany(eq("nombre", "María"));
System.out.println("Se han eliminado: " + result1.getDeletedCount());

Crear y borrar una colección

Para crear una colección utilizamos el método createCollection, asociado a la base de datos y para eliminarla, usamos el método drop asociado a la colección:

MongoClient client = MongoClients.create(URI);
MongoDatabase db = client.getDatabase(DB);
db.createCollection("new_collection");

MongoCollection<Document> collection = db.getCollection("new_collection");
collection.drop();

Listar colecciones de la base de datos

El método listCollectionNames devuelve las colecciones de la base de datos en un MongoIterable:

db.listCollectionNames()
    .forEach(System.out::println);

Crear, listar y borrar bases de datos

Para crear una base de datos se llama al método getDatabase() desde el objeto MongoClient, sin embargo, la base de datos no se creará hasta que no se inserte un documento.

Con el método listDatabaseNames() sirve para listar las bases de datos, y con el método drop se elimina

Pasar de MongoDB a Fichero

Podemos crear un fichero JSON consultando todos los documentos y añadiéndolos a un fichero con la clase BufferedWriter

MongoClient client = MongoClients.create(URI);
MongoDatabase db = client.getDatabase(DB);
MongoCollection<Document> collection = db.getCollection(COLLECTION);

try(BufferedWriter writer = new BufferedWriter(
        new FileWriter("src/main/resources/doc.json"))){
    List<Document> query = collection.find().into(new ArrayList<>());

    for (Document document :
            query) {
        writer.write(document.toJson());
        writer.newLine();
    }
} catch (IOException e) {
    System.out.println("Error");
}

client.close();

Por el contrario, si tengo un fichero JSON y quiero almacenarlo, debería leer dicho fichero con BufferedReader y insertar cada documento. Para pasar de una cadena en formato JSON a un documento, podemos usar el método estático parse de la clase Document:

MongoClient client = MongoClients.create(URI);
MongoDatabase db = client.getDatabase(DB);
MongoCollection<Document> collection = db.getCollection(COLLECTION);

try(BufferedReader reader = new BufferedReader(
        new FileReader("src/main/resources/doc.json"))){
    String str;
    Document doc;
    List<Document>  documents = new ArrayList<>();
    while((str = reader.readLine()) != null){
        doc = Document.parse(str);
        documents.add(doc);
    }

    collection.insertMany(documents);
} catch (IOException e) {
    System.out.println("Error");
}

client.close();

Clases POJOs

A veces, lo interesante de utilizar una base de datos es poder realizar conversiones con clases POJOs de un lenguaje de programación. Para ello, se debe crear la clase POJO que representaría un documento de la base de datos. Teniendo el siguiente esquema de documentos, puede ser representada por la siguiente clase POJO:

{
    "_id": "ObjectId",
    "student_id": "number",
    "scores": [
        {
            "type": "string",
            "score": "double"
        }
    ],
    "class_id": "number"
}
public class Grade {
    private ObjectId id;
    @BsonProperty("student_id")
    private Double studentId;
    @BsonProperty("class_id")
    private Integer classId;
    private List<Score> scores;

    public ObjectId getId() {
        return id;
    }

    public void setId(ObjectId id) {
        this.id = id;
    }

    public Double getStudentId() {
        return studentId;
    }

    public void setStudentId(Double studentId) {
        this.studentId = studentId;
    }

    public Integer getClassId() {
        return classId;
    }

    public void setClassId(Integer classId) {
        this.classId = classId;
    }

    public List<Score> getScores() {
        return scores;
    }

    public void setScores(List<Score> scores) {
        this.scores = scores;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Grade grade = (Grade) o;
        return id.equals(grade.id) && studentId.equals(grade.studentId)
                && classId.equals(grade.classId) && scores.equals(grade.scores);
    }

    @Override
    public int hashCode() {
        return id.hashCode() + studentId.hashCode() + classId.hashCode() + scores.hashCode();
    }
}
public class Score {
    private String type;
    private Double score;

    public String getType() {
        return type;
    }

    public void setType(String type) {
        this.type = type;
    }

    public Double getScore() {
        return score;
    }

    public void setScore(Double score) {
        this.score = score;
    }

    @Override
    public String toString() {
        return "Score{" +
                "type='" + type + '\'' +
                ", score=" + score +
                '}';
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Score score1 = (Score) o;
        return score1.type.equals(type) && score1.score.equals(score);
    }

    @Override
    public int hashCode() {
        return type.hashCode() + score.hashCode();
    }
}

Utilizamos la anotación BsonProperty(name) para hacer coincidir un atributo de la clase con una propiedad del documento, que tienen nombres diferentes.

Una vez definida la clase POJO, podemos empezar a realizar conexiones. En primer lugar, crearemos la conexión, pero en este caso en lugar de usar la ruta por defecto (tal y como hemos hecho hasta hora), necesitaremos especificarle una configuración POJO, es decir, debemos incluirle una configuración de codificación de los POJO, además de una codificación para los tipos de Java:

ConnectionString string = new ConnectionString(URI);
CodecRegistry pojoCodeRegistry = CodecRegistries.fromProviders(PojoCodecProvider.builder().automatic(true).build());
CodecRegistry codecRegistry = CodecRegistries.fromRegistries(MongoClientSettings.getDefaultCodecRegistry(), pojoCodeRegistry);
MongoClientSettings settings = MongoClientSettings.builder()
        .applyConnectionString(string)
        .codecRegistry(codecRegistry)
        .build();

MongoClient client = MongoClients.create(settings);

Analicemos cada una de las líneas del ejemplo anterior:

  • La primera línea crea una cadena de conexión, que será necesaria para crear los settings del cliente de MongoDB.
  • La segunda línea configura un registro de codificación para las clases POJOs de forma automática. De esta forma, se puede aceptar cualquier clase POJO.
  • La tercera línea añade a la configuración de codificación los registros de codificación por defecto para los tipos de Java, además para las clases POJOs.
  • En la cuarta línea creamos la configuración para el cliente de MongoDB, el cual le aplicamos la cadena de conexión (debe de ser de tipo ConnectionString, de ahí, que en la primera línea se cree la cadena con dicha clase), e indicándole los registro de codificación.
  • Por último, creamos el cliente MongoDB con toda la configuración realizada.

Una vez creado nuestro cliente, podemos acceder a la base de datos y a nuestra colección, tal y como hemos hecho hasta hora. Sin embargo, en lugar de utilizar el tipo genérico Document para la clase MongoCollection, se usaría como tipo genérico el tipo de la clase creada, además que al método getCollection se le indica también la clase a la que debe parsear:

MongoDatabase db = client.getDatabase(DB);
MongoCollection<Grade> collection = db.getCollection(COLLECTION, Grade.class);

Para poder insertar un nuevo objeto, se crea el objeto de dicha clase y se inserta haciendo uso del método insertOne. Si por el contrario, se quiere insertar varios elementos de dicha clase, se crea una lista de con los objetos de la clase y se inserta con el el método insertMany. El resto de operaciones, funcionaría exactamente igual, pero en lugar de usar objetos de clase Document, se usaría objetos de la clase Grade. Ejemplo completo:

ConnectionString string = new ConnectionString(URI);
CodecRegistry pojoCodeRegistry = fromProviders(PojoCodecProvider.builder().automatic(true).build());
CodecRegistry codecRegistry = fromRegistries(MongoClientSettings.getDefaultCodecRegistry(), pojoCodeRegistry);
MongoClientSettings settings = MongoClientSettings.builder()
        .applyConnectionString(string)
        .codecRegistry(codecRegistry)
        .build();

MongoClient client = MongoClients.create(settings);

MongoDatabase db = client.getDatabase(DB);
MongoCollection<Grade> collection = db.getCollection(COLLECTION, Grade.class);

Score score = new Score("exam", 7.5);

Grade grade = new Grade(10003.0, 10, List.of(score));


collection.insertOne(grade);

Grade findGrade = collection.find(eq("student_id", 10003d)).first();
System.out.println("Grade found:\t" + grade);

if(findGrade == null){
    System.out.println("No se encuentra");
    return;
}

List<Score> newScores = new ArrayList<>(findGrade.getScores());
Score newScore = new Score("exam", 42d);
newScores.add(newScore);
grade.setScores(newScores);
Document filterByGradeId = new Document("_id", grade.getId());
FindOneAndReplaceOptions returnDocAfterReplace = new FindOneAndReplaceOptions()
        .returnDocument(ReturnDocument.AFTER);
Grade updatedGrade = collection.findOneAndReplace(filterByGradeId, grade, returnDocAfterReplace);
System.out.println("Grade replaced:\t" + updatedGrade);

System.out.println(collection.deleteOne(filterByGradeId));

client.close();