Skip to content

outr/scarango

Folders and files

NameName
Last commit message
Last commit date

Latest commit

603f730 · Mar 17, 2024
Jul 10, 2023
Mar 1, 2024
Jul 25, 2023
Feb 29, 2024
Mar 17, 2024
Mar 17, 2024
Dec 23, 2023
Mar 16, 2024
Mar 15, 2022
Feb 8, 2017
Mar 1, 2024
Mar 3, 2024
Dec 17, 2022

Repository files navigation

Scarango

CI Gitter Maven Central Latest version

ArangoDB client written in Scala

Setup

Scarango is published to Sonatype OSS and Maven Central currently supporting Scala and Scala.js (core only) on 2.13 and 3.

Configuring the driver in SBT requires:

libraryDependencies += "com.outr" %% "scarango-driver" % "3.20.0"

Or in Mill:

ivy"com.outr::scarango-driver:3.20.0"

Introduction

Scarango wraps ArangoDB's Java library to provide lots of additional features and Scala-specific functionality. We utilize cats-effect and fs2 for a more modern asynchronous approach. Previous to 3.0, we utilized direct HTTP RESTful calls and Futures, but the performance benefits of Java's library made a migration worthwhile.

Getting Started

Although there are a few different ways we can utilize Scarango to interact with ArangoDB, the cleanest and most powerful approach is to utilize the Graph layer to set up our database structure and interact with it in a type-safe way. For example:

Database configuration

import com.outr.arango.{Document, DocumentModel, Field, Graph, Id, Index}
import com.outr.arango.collection.DocumentCollection
import com.outr.arango.query._
import fabric.rw._
import cats.effect.unsafe.implicits.global

// Case class to represent a person collection
case class Person(name: String, age: Int, _id: Id[Person] = Person.id()) extends Document[Person]

// We use the companion object to represent additional information about the collection
object Person extends DocumentModel[Person] {
  override implicit val rw: RW[Person] = RW.gen

  val name: Field[String] = field("name")
  val age: Field[Int] = field("age")

  override def indexes: List[Index] = List(
    name.index.persistent()
  )

  override val collectionName: String = "people"
}

// We represent our entire database here referencing all collections
object Database extends Graph("example") {
  val people: DocumentCollection[Person, Person.type] = vertex(Person)
}

This is the basic setup of a single-collection database. Notice the RW in the companion object. That is defined using Fabric for conversion to/from JSON for storage in the database. All fields aren't required to be defined, but it will help us when we want to write simple queries in a type-safe way or when defining things like indexes.

Initialization

The next thing we need to do is initialize the database:

Database.init().unsafeRunSync()

NOTE: We're adding .unsafeRunSync() at the end of each call for the documentation generation to get the result. Under normal circumstances this is not the ideal way to execute a cats-effect IO.

Truncate the database

We can easily clear out everything out of the database:

Database.truncate().unsafeRunSync()

Inserting into the database

A simple insert of a record into the database:

Database.people.insert(Person("User 1", 30)).unsafeRunSync()
// res2: com.outr.arango.core.CreateResult[Person] = CreateResult(
//   key = None,
//   id = None,
//   rev = None,
//   document = Person(
//     name = "User 1",
//     age = 30,
//     _id = Id(value = "Ncx2NMcIpYGtWe9sOd8byYLRuYOCPrnH", collection = "people")
//   ),
//   newDocument = None,
//   oldDocument = None
// )

We can also do batch record insertion:

Database.people.batch.insert(List(
    Person("Adam", 21),
    Person("Bethany", 19)
)).unsafeRunSync()
// res3: com.outr.arango.core.CreateResults[Person] = CreateResults(
//   results = List()
// )

You can also use the Database.people.stream to cross-stream records into the database.

Querying

In order to get the data out that we just inserted we can do a simple AQL query:

Database
  .people
  .query(aql"FOR p IN ${Database.people} RETURN p")
  .toList
  .unsafeRunSync()
// res4: List[Person] = List(
//   Person(
//     name = "User 1",
//     age = 30,
//     _id = Id(value = "Ncx2NMcIpYGtWe9sOd8byYLRuYOCPrnH", collection = "people")
//   ),
//   Person(
//     name = "Adam",
//     age = 21,
//     _id = Id(value = "b61wDJm3nMePIBhzHXpE8jOD52NRvL1V", collection = "people")
//   ),
//   Person(
//     name = "Bethany",
//     age = 19,
//     _id = Id(value = "Ps4IYa9Wfge3y0lwMw3Yxs6ln3CsW3cg", collection = "people")
//   )
// )

For an example of data conversion in the result, if we want to only get the person's name back:

Database
  .people
  .query(aql"FOR p IN ${Database.people} RETURN p.name")
  .as[String]
  .toList
  .unsafeRunSync()
// res5: List[String] = List("Adam", "Bethany", "User 1")

For more examples see the specs: https://github.com/outr/scarango/blob/master/driver/src/test/scala/spec/GraphSpec.scala

TODO

  • Improved ScalaDocs
  • Add AQL compile-time validation support (revive from 2.x)