All Downloads are FREE. Search and download functionalities are using the official Maven repository.

.circumflex-orm.2.5.source-code.relation.scala Maven / Gradle / Ivy

The newest version!
package ru.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:

    // 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)", ""))
      .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] = 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 = {
    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 {#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))
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy