← Central Abstractions | Circumflex ORM Documentation | Querying → |
The process of creating the domain model of application is refered to as data definition. It usually involves following steps:
Record
;Relation
traits (Table
or View
);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]
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 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.
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.
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:
NO_ACTION
(default),CASCADE
,RESTRICT
,SET_NULL
,SET_DEFAULT
.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.
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 → |