relation.scala

package ru.circumflex
package orm

import core._
import java.lang.reflect.Method

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 and views.

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:

// 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] {

}

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)", ""),
    true,
    Thread.currentThread.getContextClassLoader
  ).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.

  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] =
    tx.cache.cacheRecord(id, this,
      (this.AS("root")).map(r => r.criteria.add(r.PRIMARY_KEY EQ id).unique()))

  def all: Seq[R] = this.AS("root").criteria.list()

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] =>
        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)

  def init() {
    if (!_initialized) synchronized {
      if (!_initialized) try {
        findMembers(this.getClass)
        ormConf.dialect.initializeRelation(this)
        _fields.foreach(ormConf.dialect.initializeField(_))
        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 = {
    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-specific methods derived from Record 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 relation instance.")
  override def validate(): Nothing =
    throw new ORMException("This method cannot be invoked on relation instance.")
  override def INSERT_!(fields: Field[_, R]*): Nothing =
    throw new ORMException("This method cannot be invoked on relation instance.")
  override def UPDATE_!(fields: Field[_, R]*): Nothing =
    throw new ORMException("This method cannot be invoked on relation instance.")
  override def DELETE_!(): Nothing =
    throw new ORMException("This method cannot be invoked on relation instance.")
}

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

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

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[_]
}