Estoy muy contento de informar que Couchbase ahora tiene un SDK Scala soportado, lo que le permite obtener y recuperar documentos, ejecutar consultas N1QL, realizar análisis y búsquedas de texto completo - todo con Scala nativo.
En este blog voy a tocar las características clave y los principios de diseño del SDK de Scala. O si quieres ponerte en marcha de inmediato, entonces echa un vistazo a la guía de iniciación aquí. El SDK de Scala puede descargarse ahora mismo, en forma de versión alfa previa.
El SDK de Scala utiliza el mismo núcleo interno estable y de alto rendimiento que el nuevo SDK de Java 3.x, por lo que, aunque es nuevo, puede estar seguro de que estará listo para la producción cuando llegue GA.
Tratamiento funcional de errores: ¡sin excepciones!
El SDK de Scala presenta una interfaz funcional, con la mayoría de las operaciones devolviendo un Try de Scala. Esto puede ser un Éxito y contener el valor esperado, o un Fallo conteniendo un Throwable. Aunque al principio esto parece un poco verboso, y requiere cierta disciplina de programación para manejar siempre los errores, esta forma de manejo de errores se convierte rápidamente en una segunda naturaleza y le permite desarrollar con la seguridad de que una excepción aleatoria no va a ser lanzada desde el SDK.
Aquí tienes una sencilla demostración de cómo insertar un documento:
1 2 3 |
val json = JsonObject("status" -> "¡Impresionante!") val resultado: Pruebe[MutationResult] = colección.insertar("clave del documento", json) |
Y luego la concordancia de patrones en el resultado de Try:
1 2 3 4 5 |
resultado match { caso Éxito(Sr.) => println("Upsert tuvo éxito") caso Fallo(err: KeyAlreadyExistsException) => println("la clave del documento ya existe") caso Fallo(err) => println("Error: " + err) } |
(Por supuesto, una aplicación de producción gestionaría los errores adecuadamente, en lugar de limitarse a utilizar println).
Si prefieres tener algo similar al SDK de Java, que lanza excepciones, esto es fácil de simular simplemente llamando a .get() en cualquier Try, que lanzará si es un Failure:
1 |
val resultado: MutationResult = colección.insertar("clave del documento", json).consiga() |
Es fácil combinar múltiples Try's a través de flatMap. Aquí insertamos un documento, luego lo obtenemos, recuperamos su contenido convertido a la librería JSON JsonObjectSafe (más sobre esto más adelante), y finalmente obtenemos un campo llamado "status" a partir de él. Todo en un estilo completamente funcional, así que por ejemplo la operación get sólo se intentará si el upsert tiene éxito.
1 2 3 4 5 6 7 8 9 |
val resultado: Pruebe[Cadena] = colección.upsert("clave del documento", json) .flatMap(_ => colección.consiga("clave del documento")) .flatMap(_.contentAs[JsonObjectSafe]) .flatMap(_.str("status")) resultado match { caso Éxito(estado) => println(s"Couchbase es $status") caso Fallo(err) => println("Error: " + err) } |
(En aras de la concisión, en la mayoría de los ejemplos no se comprobará el resultado. Pero las aplicaciones de producción deberían, por supuesto, ABC - siempre comprobarlo).
Previsión
Fuera de la caja, el SDK soporta la nueva durabilidad más fuerte que estará disponible en Couchbase Server 6.5:
1 |
val resultado = colección.upsert("docId", json, durabilidad = Durabilidad.Mayoría) |
E incorpora soporte para colecciones, un nuevo método de organizar documentos dentro de cubos que será una característica fundamental de una futura versión de Couchbase Server (con una vista previa en breve). Pronto se podrán crear algunas colecciones de juguete con las que jugar, pero por ahora bastará con abrir la colección por defecto:
1 2 |
val grupo = Grupo.conecte("cluster-ip", "nombre de usuario", "contraseña") val colección = grupo.cubo("nombre-cubo").defaultCollection |
Esta lógica funcionará en todas las versiones soportadas de Couchbase Server (5.x y superiores).
Además, se incluye compatibilidad con OpenTracing (ahora OpenTelemetry), la plataforma que se está convirtiendo en el estándar de facto para rastrear sistemas distribuidos complejos:
1 |
val resultado = colección.upsert("docId", json, parentSpan = Algunos(mySpan)) |
API simple
El SDK utiliza parámetros con nombre y por defecto en lugar de sobrecargas, por lo que el código puede ser muy sencillo:
1 |
val resultado = colección.upsert("docId", json) |
o a medida:
1 |
val resultado = colección.upsert("docId", json, durabilidad = Durabilidad.Mayoría, caducidad = 60 minutos, parentSpan = Algunos(mySpan)) |
como usted necesita que sea.
Asíncrono
Al igual que el SDK de Java, se presentan tres API, que le ofrecen la flexibilidad necesaria para estructurar su aplicación de la forma que necesite.
La primera es la API sincrónica de bloqueo simple que has visto en los ejemplos anteriores. Esta puede ser una buena opción por defecto, pero por supuesto requiere que gestiones explícitamente tus propios hilos si es necesario.
El segundo es uno construido en torno a la programación reactiva, utilizando tipos reactivos estándar (Mono y Flux) del Proyecto Reactor.
Un ejemplo de upserting de forma reactiva:
1 2 3 |
val mono: Mono[MutationResult] = colección.reactivo.upsert("clave del documento", json) .doOnError(err => println(s"Error durante upsert: ${err}")) .doOnNext(mutationResult => println("Éxito")) |
Con la programación reactiva, no ocurrirá nada hasta que nos suscribamos al Mono. La forma más sencilla de hacer esto con fines de prueba es utilizar block(), aunque esto es una mala práctica para una aplicación real:
1 |
val resultado: MutationResult = mono.bloque() |
La programación reactiva es un tema complejo y profundo en el que no puedo profundizar demasiado aquí, pero cuando se domina proporciona una herramienta poderosa, especialmente para manejar las complejidades de los sistemas distribuidos del mundo real. En particular, la API reactiva proporciona backpressure en N1QL, FTS y consultas analíticas - por lo que si la aplicación está luchando para mantenerse al día con los datos, se ralentizará automáticamente el consumo de filas y evitar errores fuera de memoria. Animo a los curiosos a echar un vistazo a la Documentación del proyecto Reactor para saber más.
Por último, está la API asíncrona construida alrededor de Scala Futures, que proporciona un poco de la gestión de hilos y composabilidad de la programación reactiva, pero en una forma que es más familiar para muchos desarrolladores Scala. Un ejemplo simple de upsert se ve así:
1 2 3 4 5 6 |
val resultado: Futuro[MutationResult] = colección.async.upsert("clave del documento", json) resultado onComplete { caso Éxito(_) => println("Insertado con éxito") caso Fallo(excepción) => println("Error: " + excepción) } |
Flexibilidad JSON
Uno de los principales objetivos era no ceñirte a ninguna implementación JSON en particular, y el SDK de Scala funciona bien con varias librerías JSON populares de Scala, incluyendo Circe, µPickle / µJson, Json4s y Jawn. Puedes ver fragmentos de cómo usar cada una de ellas en los documentos JSONPero para abrir el apetito, aquí tiene Circe en acción: observe cómo los tipos Circe se envían y devuelven directamente al SDK:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
importar io.circe.genérico.auto._ importar io.circe.sintaxis._ val usuario = Usuario("John Smith", 29) val json: io.circe.Json = usuario.asJson val resultado: Pruebe[io.circe.Json] = colección.insertar("id", json) .flatMap(_ => colección.consiga("id")) .flatMap(doc => doc.contentAs[io.circe.Json]) resultado match { caso Éxito(contenido: io.circe.Json) => // Manejar Circe Json aquí caso Fallo(err) => println("Error: " + err) } |
JSON integrado
Tomando un enfoque de "pilas incluidas" también hay una sencilla biblioteca JSON incluida con el SDK, JsonObject. Sus principales objetivos son:
- Comodidad. No todo el mundo quiere evaluar múltiples librerías JSON antes de empezar. JsonObject es una opción decente por defecto.
- Velocidad. Nuestra evaluación comparativa interna (fuente en caso de que quieras comprobarlo por ti mismo) indica que JsonObject puede ser hasta 20 veces más rápido que la biblioteca JSON de Scala más cercana en algunas operaciones importantes. Esto se consigue sobre todo gracias a que está construido en torno a estructuras de datos JVM mutables, sencillas pero muy rápidas.
- Flexibilidad. La interfaz JsonObject por defecto lanza excepciones (el único lugar en el SDK que lo hace). O puedes convertirla fácilmente en una JsonObjectSafe, que proporciona una interfaz funcional basada en Try.
- Facilidad de uso y mutabilidad. Estamos de acuerdo con el autor de µJson que aunque la inmutabilidad suele ser deseable, no siempre es la mejor opción en el caso particular de JSON. Tratar con JSON profundamente anidado requiere herramientas funcionales como lentes, que rara vez son fáciles de leer y utilizar, por no mencionar que posiblemente incurran en una penalización de rendimiento. Además, JSON suele tratarse brevemente y en un ámbito limitado (por ejemplo, obteniendo y modificando un documento), por lo que rara vez se beneficia de la seguridad de la inmutabilidad. Así que JsonObject presenta una API mutable simple.
Puede profundizar en la Documentación sobre JsonObject. Por supuesto, si prefieres tener una librería JSON más 'típicamente Scala' con datos inmutables, tipos de datos algebraicos, lentes, cursores y otras bondades funcionales, entonces una de las otras librerías soportadas puede ser una mejor elección - la elección es tuya, y puedes fácilmente mezclar y combinar múltiples librerías JSON en la misma aplicación.
Apoyo directo a las clases de casos
Aunque el SDK de Scala tiene un gran soporte para JSON, a veces es más fácil y preferible trabajar directamente con las clases de casos de Scala.
Con sólo añadir este pequeño trozo de boilerplate para tu clase case, el SDK puede generar automáticamente la lógica de serialización:
1 2 3 4 5 |
caso clase Usuario(nombre: Cadena, edad: Int) objeto Usuario { implícito val códec: Códec[Usuario] = Códecs.códec[Usuario] } |
Y entonces usted puede fácilmente enviar esa clase de caso directamente a, y recibirla de vuelta de, el SDK:
1 2 3 |
val r: Pruebe[Usuario] = colección.insertar("docId", Usuario("John Smith", 29)) .flatMap(_ => colección.consiga("docId") .flatMap(doc => doc.contentAs[Usuario]) |
Bajo el capó esto se convierte y se almacena en Couchbase Server como el JSON que usted esperaría:
1 |
{"nombre":"John Smith","edad":29} |
Se trata de JSON normal y corriente, sin metadatos de serialización adicionales o similares, por lo que puede escribirse libremente como una clase de caso y leerse en una biblioteca JSON o por otro SDK, o viceversa.
Echa un vistazo a la documentación sobre el uso de clases de casos para saber más.
Consultas sencillas
Hemos facilitado al máximo el uso de Couchbase Analytics, N1QL y Full Text Search. Aquí hay un ejemplo de una consulta N1QL donde las filas se convierten en JsonObject.
1 2 3 4 5 6 7 8 |
grupo.consulta("""select * from `viaje-muestra` limit 10;""") .flatMap(_.allRowsAs[JsonObject]) match { caso Éxito(filas: Seq[JsonObject]) => filas.foreach(fila => println(fila)) caso Fallo(err) => println(s"Error: $err") } |
Y por supuesto es posible obtener esos resultados como cualquiera de los tipos JSON soportados arriba, o directamente como clases de casos Scala.
Y como ya se ha mencionado, el uso de la API reactiva significa que se obtiene backpressure automático de forma gratuita, lo que garantiza que la aplicación consuma filas a un ritmo manejable y no pueda encontrarse con problemas de falta de memoria en consultas muy grandes.
Si quieres empezar a utilizar el SDK Scala de Couchbase, echa un vistazo a la sección guía de inicio rápido. Ya está disponible, aunque en fase alfa. Puede que haya algunos cambios de última hora antes de pasar a GA, pero por otro lado este es el momento perfecto para probarlo y darnos tu opinión dejando un comentario a continuación, o planteándolo en nuestros foros, Twitter o gitter. Nada es inamovible y agradecemos la oportunidad de hacer algunos cambios y convertirlo en el mejor SDK posible.