← Central Abstractions Circumflex ORM Documentation Querying →

Data Definition

The process of creating the domain model of application is refered to as data definition. It usually involves following steps:

Here's a simple example of fictional domain model:

class Country extends Record[String, Country] {
  val code = "code".VARCHAR(2).NOT_NULL
  val name = "name".TEXT.NOT_NULL

  def PRIMARY_KEY = code
  def relation = Country
}

object Country extends Country with Table[String, Country]

Record

In this example the Country table will have two fields, code and name. The first type parameter, String, designates the type of primary key (we refer to this type as PK). The second type parameter points to class itself to ensure type safety. The Record class has two abstract methods which should be implemented: PRIMARY_KEY and relation.

The PRIMARY_KEY method points to Field which type matches PK (String in our example). Primary key uniquely identifies a record in database table. Unfortunately, Circumflex ORM does not support composite primary keys yet.

The relation points to companion object which corresponds to record. It must have the same name as record class and should extend a record itself to inherit all its fields.

The body of record class contains field definitions. A field should be a public immutable (val) member of record class. Each field corresponds to a column in database table.

As the example above shows, the syntax of field definition closely resembles classic DDL for generating database schema for tables: you specify the column name with String, then you call one of the methods to create a field of certain type, then you optionally call one of methods that change the definition of target column.

Generally, spaces may be used to delimit method calls and improve readability of column definitions. However, sometimes Scala compiler forces you to use dot-notation:

val name = "name".TEXT.NOT_NULL

Following methods are used to create field definitions:

Method SQL type Scala type Implementing class
INTEGER INTEGER Int IntField
BIGINT BIGINT Long LongField
DOUBLE(precision: Int, scale: Int) NUMERIC(p, s) Double DoubleField
NUMERIC(precision: Int, scale: Int roundingMode: BigDecimal.RoundingMode.RoundingMode) NUMERIC(p, s) scala.math.BigDecimal NumericField
TEXT TEXT String TextField
VARCHAR(length: Int) VARCHAR(l) String TextField
BOOLEAN BOOLEAN Boolean BooleanField
DATE DATE java.util.Date DateField
TIME TIME java.util.Date TimeField
TIMESTAMP TIMESTAMP java.util.Date TimestampField

In the table above the default SQL types show the types defined in default dialect, which can be overriden in vendor-specific dialects. Besides it is possible to define a field with custom SQL type by subclassing the Field class. Refer to Circumflex ORM API documentation for details.

Since version 2.0 genearated columns will not have NOT NULL constraints by default (this behavior is consistent with SQL specifications). You should call NOT_NULL method to express NOT NULL constraint in column definition:

val mandatory = "mandatory".TEXT.NOT_NULL
val optional = "optional".TEXT

You can optionally initialize a field with value with NOT_NULL:

val createdAt = "created_at".TIMESTAMP.NOT_NULL(new Date)

You can also specify the default expression for the field, it will be rendered in database column definition:

val radius = "radius".NUMERIC.NOT_NULL
val square = "square".NUMERIC.NOT_NULL.DEFAULT("PI() * (radius ^ 2)")

You can also create a single-column unique constraint using the UNIQUE method:

val login = "login".VARCHAR(64).NOT_NULL.UNIQUE

Fields operate with values. The syntax for accessing and setting values is self-descriptive:

val age = "age".INTEGER  // Field[Int, R]
// accessing
age.value                     // Option[Int]
age.get                       // Option[Int]
age()                         // Int
age.getOrElse(default: Int)   // Int
age.null_?                    // Boolean
// setting
age := 25
age.set(25)
age.set(Some(25))
age.set(None)
age.setNull

It is a good practice to place domain-specific logic inside record classes. The following example shows the most trivial case: overriding toString and providing alternative constructor:

class Country extends Record[String, Country] {
  def PRIMARY_KEY = code
  def relation = Country
  // Constructor shortcuts
  def this(code: String, name: String) = {
    this()
    this.code := code
    this.name := name
  }
  // Fields
  val code = "code" VARCHAR(2) DEFAULT("'ch'")
  val name = "name" TEXT
  // Miscellaneous
  override def toString = name.getOrElse("Unknown")
}

Relation

Relation is defined as a companion object for corresponding record. As mentioned before, the relation object should have the same name as its corresponding record class, should extend that record class and should mix in one of the Relation traits (Table or View):

class Country extends Record[String, Country] {
  def relation = Country
  // ...
}
object Country extends Country with Table[String, Country]

You can place the definitions of constraints and indexes inside the body of relation, they should be public immutable (val) members of relation:

object Country extends Country with Table[String, Country] {
  // a named UNIQUE constraint
  val codeKey = CONSTRAINT("code_uniq").UNIQUE(this.code)
  // a UNIQUE constraint with default name
  val codeKey = UNIQUE(this.code)
  // a named CHECK constraint:
  val codeChk = CONSTRAINT("code_chk").CHECK("code IN ('ch', 'us', 'uk', 'fr', 'es', 'it', 'pt')")
  // a named FOREIGN KEY constraint:
  val fkey = CONSTRAINT("eurozone_code_fkey").FOREIGN_KEY(EuroZone, this.code -> EuroZone.code)
  // an index:
  val idx = "country_code_idx".INDEX("LOWER(code)").USING("btree").UNIQUE
}

Consult Circumflex ORM API Documentation for other definition options.

The relation object is also the right place for various querying methods:

object User extends Table[Long, User] {
  def findByLogin(l: String): Option[User] = (this AS "u").map(u =>
      SELECT(u.*).FROM(u).WHERE(u.login LIKE l).unique)
}

See querying, data manipulation and Criteria API sections for more information.

Generating Identifiers

Circumflex ORM allows you to use database-generated identifiers as primary keys. Let's take a look at following data definition snippet:

class City extends Record[Long, City] with IdentityGenerator[Long, City] {
  val id = "id".BIGINT.NOT_NULL.AUTO_INCREMENT
  val name = "name".TEXT.NOT_NULL
  def PRIMARY_KEY = id
  def relation = City
}

object City extends City with Table[Long, City]

This snippet shows a surrogate primary key example. The value of id is generated when a record is inserted. Then additional SQL select is issued to read this generated value.

For more information refer to Circumflex ORM API Documentation.

Associations

An association provides a way to link one relation with another.

class City extends Record[Long, City] {
  val country = "country_code".TEXT.REFERENCES(Country).ON_DELETE(CASCADE).ON_UPDATE(NO_ACTION)
}

As the example above shows, associations are created from fields using the REFERENCES method. The type of the field must match the type of primary key of referenced relation.

Associations also implicitly add foreign key constraint to table's definition. The cascading actions can be specified by invoking ON_DELETE and ON_UPDATE with one of the following arguments:

Associations are directed: the relation that owns an association is often refered to as a child relation, while the relation to which an associations references is often refered to as a parent relation.

Like with regular field, you can set an retrieve the association's value:

// accessing
country.value                       // Option[Country]
country.get                         // Option[Country]
country()                           // Country
country.getOrElse(default: Country) // Country
country.null_?                      // Boolean
// setting
country := switzerland
country.set(switzerland)
country.set(Some(switzerland))
country.set(None)
country.setNull

Associations do not store objects themselves. Instead they store the primary key of an object in their internal field. You can access and set this value directly using the field method:

country.field   // Field[String, R]
country.field := "ch"

When you access association using its get, apply, value or getOrElse methods, the actual record is returned from cache of current transaction. However, if record does not exist in cache yet, a transparent SQL select will be issued to fetch this record. This technique is usually refered to as lazy initialization or lazy fetching:

val c = new City
c.id := 16
c.country()   // a SELECT query is executed to retrieve a Country
              // for the City with id = 16
c.country()   // further selects are not issued

The other side of association can optionally define an inverse association using following syntax:

class Country extends Record[String, Country] {
  def cities = inverseMany(City.country)
}

Inverse associations are not represented by field in their relation, they are initialized by issuing the SELECT statement against child relation:

val c = new Country
c.code := 'ch'
c.cities()   // a SELECT query is executed to retrieve a set of City objects
             // which have country_code = 'ch'
c.cities()   // further selects are not issued

Here we have the so-called «one-to-many» relationship. The «one-to-one» relationship is simulated by placing a unique constraint on association (in child table) and using inverseOne in parent table.

You can also perform association prefetching for both straight and inverse associations using the Criteria API.

Validation

A record can be optionally validated before it is saved into database.

The validation is performed using one or more validators, functions which take a Record and return Option[Msg]: None if validation succeeds or Some[Msg] otherwise. In case of failed validation the Msg object is used to describe the exact problem. Refer to Circumflex Messages API Documentation to find out how to work with messages.

Validators are added to the validation object inside relation:

object Country extends Table[String, Country] {
  validation.add(r => ...)
      .add(r => ...)
}

There are several predefined validators available for your convenience:

object Country extends Table[String, Country] {
  validation.notNull(_.code)
      .notEmpty(_.code)
      .pattern(_.code, "(?i:[a-z]{2})")
}

A record is validated when either validate or validate_! is invoked. The first one returns Option[MsgGroup]:

rec.validate match {
  case None => ...            // validation succeeded
  case Some(errors) => ...    // validation failed
}

The second one does not return anything, but throws ValidationException if validation fails.

The validate_! method is also called when a record is being saved into database, read more in Insert, Update & Delete section.

It is also fairly easy to implement custom validators. Following example shows a validator for checking unique email addresses:

object Account extends Table[Long, Account] {
  validation.add(r => criteria
      .add(r.email EQ r.email())
      .unique
      .map(a => new Msg(r.email.uuid + ".unique")))
}
← Central Abstractions Circumflex ORM Documentation Querying →