rcumflex.circumflex-orm.3.0-RC1.source-code.relation.scala Maven / Gradle / Ivy
The newest version!
package pro.savant.circumflex
package orm
import core._, cache._
import java.lang.reflect.Method
import collection.mutable.HashMap
/*!# Relations
Like records, relations are alpha and omega of relational theory and, therefore,
of Circumflex ORM API.
In relational model a relation is a data structure which consists of a heading and
an unordered set of rows which share the same type. Classic relational databases
often support two type of relations, [tables](#table) and [views](#view).
In Circumflex ORM the relation contains record metadata and various operational
information. There should be only one relation instance per application, so
by convention the relations should be the companion objects of corresponding
records:
``` {.scala}
// Record definition
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
}
// Corresponding relation definition
object Country extends Country with Table[String, Country] {
// This is a right place for encapsulating queries, adding constraints,
// defining validation, etc.
}
```
The relation should also inherit the structure of corresponding record so that
it could be used to compose predicates and other expressions in a DSL-style.
*/
trait Relation[PK, R <: Record[PK, R]]
extends Record[PK, R]
with SchemaObject { this: R =>
protected var _initialized = false
/*!## Commons
If the relation follows default conventions of Circumflex ORM (about
companion objects), then record class is inferred automatically. Otherwise
you should override the `recordClass` method.
*/
val _recordClass: Class[R] = Class.forName(
this.getClass.getName.replaceAll("\\$(?=\\Z)", ""))
.asInstanceOf[Class[R]]
def recordClass: Class[R] = _recordClass
/*! By default the relation name is inferred from `recordClass` by replacing
camelcase delimiters with underscores (for example, record with class
`ShoppingCartItem` will have a relation with name `shopping_cart_item`).
You can override `relationName` to use different name.
*/
val _relationName = camelCaseToUnderscore(recordClass.getSimpleName)
def relationName = _relationName
def qualifiedName = ormConf.dialect.relationQualifiedName(this)
/*! Default schema name is configured via the `orm.defaultSchema`
configuration property. You may provide different schema for different
relations by overriding their `schema` method.
*/
def schema: Schema = ormConf.defaultSchema
/*! The `isReadOnly` method is used to indicate whether the DML operations
are allowed with this relation. Tables usually allow them and views usually don't.
*/
def isReadOnly: Boolean
/*! The `isAutoRefresh` method is used to indicate whether the record
should be immediately refreshed after every successful `INSERT` or `UPDATE`
operation. By default it returns `false` to maximize performance.
However, if the relation contains columns with auto-generated values
(e.g. `DEFAULT` clauses, auto-increments, triggers, etc.)
then you should override this method to return `true`.
*/
def isAutoRefresh: Boolean = false
/*! Use the `AS` method to create a relation node from this relation
with an explicit alias. */
def AS(alias: String): RelationNode[PK, R] = new RelationNode(this).AS(alias)
def findAssociation[T, F <: Record[T, F]]
(relation: Relation[T, F]): Option[Association[T, R, F]] =
associations.find(_.parentRelation == relation)
.asInstanceOf[Option[Association[T, R, F]]]
val validation = new RecordValidator[PK, R]()
/*!## Simple queries
Following methods will help you perform common querying tasks:
* `get` retrieves a record either from cache or from database
by specified `id`;
* `all` retrieves all records.
*/
def get(id: PK): Option[R] = cache.getOption(id.toString, {
val r = this.AS("root")
r.criteria.add(r.PRIMARY_KEY EQ id).unique()
})
def all: Seq[R] = this.AS("root").criteria.list()
/*!## Cache
Caches are used to temporarily store records at transaction-scoped cache
to avoid unnecessary selects (particularly, when consequently using `get(id)`
method or accessing associatios).
By mixing in `Cacheable` trait, transaction-level caches are turned into
application-level caches.
*/
def cacheName = ormConf.prefix(":") + qualifiedName
def cache: Cache[R] = tx.cache.forRelation(this)
/*!## Metadata
Relation metadata contains operational information about it's records by
introspecting current instance upon initialization.
*/
protected var _methodsMap: Map[Field[_, R], Method] = Map()
def methodsMap: Map[Field[_, R], Method] = {
_init()
_methodsMap
}
protected var _fields: List[Field[_, R]] = Nil
def fields: Seq[Field[_, R]] = {
_init()
_fields
}
protected var _associations: List[Association[_, R, _]] = Nil
def associations: Seq[Association[_, R, _]] = {
_init()
_associations
}
protected var _constraints: List[Constraint] = Nil
def constraints: Seq[Constraint] = {
_init()
_constraints
}
protected var _indexes: List[Index] = Nil
def indexes: Seq[Index] = {
_init()
_indexes
}
private def findMembers(cl: Class[_]) {
if (cl != classOf[Any]) findMembers(cl.getSuperclass)
cl.getDeclaredFields.flatMap { f =>
try {
Some(cl.getMethod(f.getName))
} catch {
case e: Exception =>
None
}
}.foreach(processMember(_))
}
private def processMember(m: Method) {
val cl = m.getReturnType
if (classOf[ValueHolder[_, R]].isAssignableFrom(cl)) {
val vh = m.invoke(this).asInstanceOf[ValueHolder[_, R]]
processHolder(vh, m)
} else if (classOf[Constraint].isAssignableFrom(cl)) {
val c = m.invoke(this).asInstanceOf[Constraint]
this._constraints ++= List(c)
} else if (classOf[Index].isAssignableFrom(cl)) {
val i = m.invoke(this).asInstanceOf[Index]
this._indexes ++= List(i)
}
}
private def processHolder(vh: ValueHolder[_, R], m: Method) {
vh match {
case f: Field[_, R] if (!this._fields.contains(f)) =>
this._fields ++= List(f)
if (f.isUnique) this.UNIQUE(f)
this._methodsMap += (f -> m)
case a: Association[_, R, _] =>
this._associations ++= List[Association[_, R, _]](a)
this._constraints ++= List(associationFK(a))
processHolder(a.field, m)
case _ =>
}
}
private def associationFK(a: Association[_, R, _]) =
CONSTRAINT(relationName + "_" + a.name + "_fkey")
.FOREIGN_KEY(a.field)
.REFERENCES(a.parentRelation, a.parentRelation.PRIMARY_KEY)
.ON_DELETE(a.onDeleteClause)
.ON_UPDATE(a.onUpdateClause)
protected def _init() {
if (!_initialized) synchronized {
if (!_initialized) try {
// Introspect record/relation members
findMembers(this.getClass)
// Ask dialect to initialize relation
ormConf.dialect.initializeRelation(this)
_fields.foreach(ormConf.dialect.initializeField(_))
// We are done
this._initialized = true
} catch {
case e: NullPointerException =>
throw new ORMException(
"Failed to initialize " + relationName + ": " +
"possible cyclic dependency between relations. " +
"Make sure that at least one side uses weak reference to another " +
"(change `val` to `lazy val` for fields and " +
"to `def` for inverse associations).", e)
case e: Exception =>
throw new ORMException("Failed to initialize " + relationName + ".", e)
}
}
}
def copyFields(src: R, dst: R) {
fields.foreach { f =>
val value = getField(src, f.asInstanceOf[Field[Any, R]]).value
getField(dst, f.asInstanceOf[Field[Any, R]]).set(value)
}
}
def getField[T](record: R, field: Field[T, R]): Field[T, R] =
methodsMap(field).invoke(record) match {
case a: Association[T, R, _] => a.field
case f: Field[T, R] => f
case _ => throw new ORMException("Could not retrieve a field.")
}
/*! You can declare explicitly that certain associations should always be prefetched
whenever a relation participates in a `Criteria` query. To do so simply call the
`prefetch` method inside relation initialization code. Note that the order of
association prefetching is important; for more information refer to `Criteria`
documentation.
*/
protected var _prefetchSeq: Seq[Association[_, _, _]] = Nil
def prefetchSeq = _prefetchSeq
def prefetch[K, C <: Record[_, C], P <: Record[K, P]]
(association: => Association[K, C, P]): this.type =
aliasStack.preserve { () =>
this._prefetchSeq ++= List(association)
this
}
/*!## Constraints & Indexes Definition
Circumflex ORM allows you to define constraints and indexes inside the
relation body using DSL style.
*/
def CONSTRAINT(name: String) = new ConstraintHelper(name, this)
def UNIQUE(columns: ValueHolder[_, R]*) =
CONSTRAINT(relationName + "_" + columns.map(_.name).mkString("_") + "_key")
.UNIQUE(columns: _*)
/*!## Auxiliary Objects
Auxiliary database objects like triggers, sequences and stored procedures
can be attached to relation using `addPreAux` and `addPostAux` methods:
the former one indicates that the auxiliary object will be created before
the creating of all the tables, the latter indicates that the auxiliary
object creation will be delayed until all tables are created.
*/
protected var _preAux: List[SchemaObject] = Nil
def preAux: Seq[SchemaObject] = _preAux
def addPreAux(objects: SchemaObject*): this.type = {
objects.foreach(o => if (!_preAux.contains(o)) _preAux ++= List(o))
this
}
protected var _postAux: List[SchemaObject] = Nil
def postAux: Seq[SchemaObject] = _postAux
def addPostAux(objects: SchemaObject*): this.type = {
objects.foreach(o => if (!_postAux.contains(o)) _postAux ++= List(o))
this
}
/*!## Events
Relation allows you to attach listeners to certain lifecycle events of its records.
Following events are available:
* `beforeInsert`
* `afterInsert`
* `beforeUpdate`
* `afterUpdate`
* `beforeDelete`
* `afterDelete`
*/
protected var _beforeInsert: Seq[R => Unit] = Nil
def beforeInsert = _beforeInsert
def beforeInsert(callback: R => Unit): this.type = {
this._beforeInsert ++= List(callback)
this
}
protected var _afterInsert: Seq[R => Unit] = Nil
def afterInsert = _afterInsert
def afterInsert(callback: R => Unit): this.type = {
this._afterInsert ++= List(callback)
this
}
protected var _beforeUpdate: Seq[R => Unit] = Nil
def beforeUpdate = _beforeUpdate
def beforeUpdate(callback: R => Unit): this.type = {
this._beforeUpdate ++= List(callback)
this
}
protected var _afterUpdate: Seq[R => Unit] = Nil
def afterUpdate = _afterUpdate
def afterUpdate(callback: R => Unit): this.type = {
this._afterUpdate ++= List(callback)
this
}
protected var _beforeDelete: Seq[R => Unit] = Nil
def beforeDelete = _beforeDelete
def beforeDelete(callback: R => Unit): this.type = {
this._beforeDelete ++= List(callback)
this
}
protected var _afterDelete: Seq[R => Unit] = Nil
def afterDelete = _afterDelete
def afterDelete(callback: R => Unit): this.type = {
this._afterDelete ++= List(callback)
this
}
/*!## Equality & Others
Two relations are considered equal if they share the record class
and the same name.
The `hashCode` method delegates to record class.
The `canEqual` method indicates that two relations share
the same record class.
Record-manipulation methods derived from `Record` which apply specifically
to records throw an exception when invoked against relation.
*/
override def equals(that: Any) = that match {
case that: Relation[_, _] =>
this.recordClass == that.recordClass &&
this.relationName == that.relationName
case _ => false
}
override def hashCode = this.recordClass.hashCode
override def canEqual(that: Any): Boolean = that match {
case that: Relation[_, _] =>
this.recordClass == that.recordClass
case that: Record[_, _] =>
this.recordClass == that.getClass
case _ => false
}
override def refresh(): Nothing =
throw new ORMException("This method cannot be invoked on the relation.")
override def validate(): Nothing =
throw new ORMException("This method cannot be invoked on the relation.")
override def INSERT_!(fields: Field[_, R]*): Nothing =
throw new ORMException("This method cannot be invoked on the relation.")
override def UPDATE_!(fields: Field[_, R]*): Nothing =
throw new ORMException("This method cannot be invoked on the relation.")
override def DELETE_!(): Nothing =
throw new ORMException("This method cannot be invoked on the relation.")
}
/*!## Implicit Conversions
`Relation` is converted to `RelationNode` implicitly if necessary. If this happens,
the default alias `this` will be assigned to the node. Use `AS` method perform the
explicit conversion if you need to specify an alias manually.
*/
object Relation {
implicit def toNode[PK, R <: Record[PK, R]]
(relation: Relation[PK, R]): RelationNode[PK, R] =
new RelationNode[PK, R](relation)
}
/*!# Table {#table}
The `Table` class represents plain-old database table which will be created to store
records.
*/
trait Table[PK, R <: Record[PK, R]] extends Relation[PK, R] { this: R =>
def isReadOnly: Boolean = false
def objectName: String = "TABLE " + qualifiedName
def sqlCreate: String = {
_init()
ormConf.dialect.createTable(this)
}
def sqlDrop: String = {
_init()
ormConf.dialect.dropTable(this)
}
}
/*!# View {#view}
The `View` class represents a database view, whose definition is designated by
the `query` method. By default we assume that views are not updateable, so
DML operations are not allowed on view records. If you implement updateable
views on backend somehow (with triggers in Oracle or rules in PostgreSQL),
override the `isReadOnly` method accordingly.
*/
trait View[PK, R <: Record[PK, R]] extends Relation[PK, R] { this: R =>
def isReadOnly: Boolean = true
def objectName: String = "VIEW " + qualifiedName
def sqlDrop: String = {
_init()
ormConf.dialect.dropView(this)
}
def sqlCreate: String = {
_init()
ormConf.dialect.createView(this)
}
def query: Select[_]
}
/*!# Application-scoped cache
Circumflex ORM lets you organize application-scope cache
(backed by Terracotta Ehcache) for any relation of your application:
just mix in the `Cacheable` trait into your relation.
*/
trait Cacheable[PK, R <: Record[PK, R]] extends Relation[PK, R] { this: R =>
protected object _cache extends HashMap[String, Cache[_]] {
def forName(name: String): Cache[R] = {
get(name) match {
case Some(cache: Cache[R]) => cache
case _ =>
val cache = new Ehcache[R](name)
update(name, cache)
cache
}
}
}
override def cache: Cache[R] = {
val c = _cache.forName(cacheName)
// add it to current transaction scope
tx.cache.update("RELATION:" + cacheName, c)
c
}
afterInsert { r =>
cache.evict(r.PRIMARY_KEY().toString)
cache.put(r.PRIMARY_KEY().toString, r)
}
afterUpdate { r =>
cache.evict(r.PRIMARY_KEY().toString)
cache.put(r.PRIMARY_KEY().toString, r)
}
afterDelete(r => cache.evict(r.PRIMARY_KEY().toString))
}