Skip to content

Latest commit

 

History

History
141 lines (97 loc) · 7.11 KB

File metadata and controls

141 lines (97 loc) · 7.11 KB
CI Test coverage(%) Code quality Stable version ScalaDoc Chat Open issues Average issue resolution time
Build Status Coverage Status Codacy Rating Maven Central ScalaDoc Gitter Percentage of issues still open Average time to resolve an issue

One of the most convenient features of phantom is that you can drive your schema directly from the code. So instead of having to create the schema first inside Cassandra and then struggle to write matching code, you can drive your entire database layer from the Scala code.

This is called schema auto-generation, and it's pretty much self explanatory. Phantom will provide you with simple methods to allow you to drive the schema from the code. Let's explore this simple schema and the database below.

import scala.concurrent.Future
import com.outworkers.phantom.dsl._

case class User(id: UUID, email: String, name: String)

abstract class Users extends Table[Users, User] {
  object id extends UUIDColumn with PartitionKey
  object email extends StringColumn
  object name extends StringColumn

  def findById(id: UUID): Future[Option[User]] = {
    select.where(_.id eqs id).one()
  }
}

abstract class UsersByEmail extends Table[UsersByEmail, User] {
  object email extends StringColumn with PartitionKey
  object id extends UUIDColumn
  object name extends StringColumn

  def findByEmail(email: String): Future[Option[User]] = {
    select.where(_.email eqs email).one()
  }
}

class AppDatabase(
  override val connector: CassandraConnection
) extends Database[AppDatabase](connector) {
  object users extends Users with Connector
  object usersByEmail extends UsersByEmail with Connector
}

The simplest level of auto-generation is at table level. Let's look at how we could create the schema automatically.

object TestConnector {
  val connector = ContactPoint.local
    .noHeartbeat()
    .keySpace("myapp_example")
}

object TestDatabase extends AppDatabase(TestConnector.connector)

More advanced indexing scenarios

Secondary indexes are a non scalable flavour of Cassandra indexing that allows us to query certain columns without needing to duplicate data. They do not scale very well at well, but they remain useful for tables where the predicted cardinality for such records is very small.

That aside, it's worth noting phantom is capable of auto-generating your CQL schema and initialising all your indexes automatically, and this functionality is exposed through the exact same table.create.future().

import com.outworkers.phantom.dsl._

case class TestRow(
  key: String,
  list: List[String],
  setText: Set[String],
  mapTextToText: Map[String, String],
  setInt: Set[Int],
  mapIntToText: Map[Int, String],
  mapIntToInt: Map[Int, Int]
)

abstract class IndexedCollectionsTable extends Table[IndexedCollectionsTable, TestRow] {

  object key extends StringColumn with PartitionKey

  object list extends ListColumn[String]

  object setText extends SetColumn[String] with Index

  object mapTextToText extends MapColumn[String, String] with Index

  object setInt extends SetColumn[Int]

  object mapIntToText extends MapColumn[Int, String] with Index with Keys

  object mapIntToInt extends MapColumn[Int, Int] with Index with Entries
}

Automated initialisation of an entire database

Using Database objects and injecting them into your controllers and other parts where they need to exist is not something that we have designed just for application layering purposes. It is indeed a very powerful feature that you can perfectly encapsulate the scope where a session exists using a Database object, but another very powerful feature is the ability to auto-generate and sync the schema for entire databases with a single method.

Let's have a look at this example taken from the dsl module inside phantom. We use this for testing purposes, and all phantom tests are written to run against this database, which is then injected into all the test suites using a provider trait, namely DatabaseProvider.

To initialise the entire database in a single call, you can use a single call to the autocreate().future() method. If you are using the Twitter API provided via the phantom-finagle module, you can also call autocreate.execute() and get a Twitter future back, provided you have imported the right implicits.

Await.result(database.autocreate.future(), 10.seconds)
class TestDatabase(override val connector: KeySpaceDef) extends DatabaseImpl(connector) {
  object articles extends ConcreteArticles with connector.Connector
  object articlesByAuthor extends ConcreteArticlesByAuthor with connector.Connector

  object basicTable extends ConcreteBasicTable with connector.Connector
  object enumTable extends ConcreteEnumTable with connector.Connector
  object namedEnumTable extends ConcreteNamedEnumTable with connector.Connector
  object clusteringTable extends ConcreteClusteringTable with connector.Connector
  object complexClusteringTable extends ConcreteComplexClusteringTable with connector.Connector
  object brokenClusteringTable extends ConcreteBrokenClusteringTable with connector.Connector
  // ..
}

Specifying custom table creation options during auto-generation

In some instances you may want to override the default query used to create a table, and that could be for the purpose of specifying more advanced options at query creation time.

The default query used by phantom during auto-generation is: database.myTable.create.ifNotExists()

Which will produce the following CQL equivalent: CREATE TABLE IF NOT EXISTS mytable, with no options specified.