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:
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
:
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()
:
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:
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:
De esta forma podemos obtener todos los documentos que tenga 2ºDAM, ordenados de forma descendiente por el 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:
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:
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étodoset(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, ounset(prop)
, elimina la propiedad del documento. Se puede combinar varias actualizaciones haciendo uso del métodocombine
. - 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 claseUpdateOptions
.
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:
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:
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();