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

rcumflex.circumflex-orm.3.0-RC1.source-code.record.scala Maven / Gradle / Ivy

The newest version!
package pro.savant.circumflex
package orm

import core._, cache._

/*!# Record

The record is a central abstraction in Circumflex ORM. Every object persisted into
database should extend the `Record` class. It provides data definition methods
necessary to create a domain model for your application as well as methods for
saving and updating your record to backend database.

Circumflex ORM is all about type safety and domain-specific languages (at a first
glance the record definition may seem a little verbose). Here's the sample definition
of fictional record `Country`:

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

  def PRIMARY_KEY = code
  val relation = Country
}
```

*/
abstract class Record[PK, R <: Record[PK, R]]
    extends Equals with Cached { this: R =>

  def expired = false

  implicit def fieldPair2cmp[T1, T2](pair: (Field[T1, R], Field[T2, R])): FieldComposition2[T1, T2, R] =
    composition(pair._1, pair._2)

  implicit def str2ddlHelper(str: String): DefinitionHelper[R] =
    new DefinitionHelper(str, this)

  /*!## Record State

  Records in relational theory are distinguished from each other by the value
  of their _primary key_. You should specify what field hold the primary key
  of your record by implementing the `PRIMARY_KEY` method.

  The `isTransient` method indicates, whether the record was not persisted
  into a database yet or it was. The default logic is simple: if the primary
  key contains `null` then the record is _transient_ (i.e. not persisted),
  otherwise the record is considered persistent.

  The `relation` method points to the relation from which a record came or
  to which it should go.

  In general this method should point to the companion object. However, if
  you do not convey to Circumflex ORM conventions, you may specify another
  object which will act a relation for this type of records.
  */
  def PRIMARY_KEY: ValueHolder[PK, R]

  def isTransient: Boolean = PRIMARY_KEY.isEmpty

  def relation: Relation[PK, R]

  /*!## Persistence & Validation */

  /*! The `evict` method removes the record from cache. */
  def evict(): this.type = {
    if (!isTransient) {
      val key = PRIMARY_KEY().toString
      relation.cache.evict(key)
      CACHE_LOG.debug("Evicting " + relation.qualifiedName + " #" + key)
    }
    this
  }

  /*! The `refresh` method is used to synchronize an already persisted record
  with its state in backend.

  It evicts the record from cache and performs SQL `SELECT`
  using primary key-based predicate.
  */
  def refresh(): this.type = if (isTransient)
    throw new ORMException("Could not refresh transient record.")
  else {
    val root = relation.AS("root")
    val id = PRIMARY_KEY()
    relation.cache.evict(id.toString)
    SELECT(root.*).FROM(root).WHERE(root.PRIMARY_KEY EQ id).unique() match {
      case Some(r: R) =>
        relation.copyFields(r, this)
        this
      case _ =>
        throw new ORMException("Could not refresh a record because " +
            "it is missing in the backend.")
    }
  }


  /* The `INSERT_!`, `UPDATE_!` and `DELETE_!` methods are used
  to insert, update or delete a single record.

  The `INSERT` and `UPDATE` do the same as their equivalents except that validation
  is performed before actual execution.

  When inserting new record into database the primary key should be generated.
  It is done either by polling database, by supplying `NULL` in primary key
  and then querying last generated identifier or manually by application.

  The default implementation relies on application-assigned identifiers;
  to use different strategy mix in one of the `Generator` traits or simply
  override the `_persist` method.
  */
  def INSERT_!(fields: Field[_, R]*): Int = if (relation.isReadOnly)
    throw new ORMException("The relation " + relation.qualifiedName + " is read-only.")
  else {
    // Execute events
    relation.beforeInsert.foreach(c => c(this))
    // Prepare and execute query
    val result = _persist(evalFields(fields))
    // Update cache
    relation.cache.put(PRIMARY_KEY().toString, this)
    relation.associations.foreach(_.inverseCache.invalidate())
    // Execute events
    relation.afterInsert.foreach(c => c(this))
    result
  }

  def INSERT(fields: Field[_, R]*): Int = {
    validate_!()
    INSERT_!(fields: _*)
  }

  protected def _persist(fields: Seq[Field[_, R]]): Int = PRIMARY_KEY.value match {
    case Some(id: PK) =>
      val result = new Insert(relation, fields.filter(!_.isEmpty)).execute()
      if (relation.isAutoRefresh) refresh()
      result
    case _ => throw new ORMException("Application-assigned identifier is expected. " +
        "Use one of the generators if you wish identifiers to be generated automatically.")
  }

  def UPDATE_!(fields: Field[_, R]*): Int = if (relation.isReadOnly)
    throw new ORMException("The relation " + relation.qualifiedName + " is read-only.")
  else {
    if (PRIMARY_KEY.isEmpty)
      throw new ORMException("Update is only allowed with non-null PRIMARY KEY field.")
    // Execute events
    relation.beforeUpdate.foreach(c => c(this))
    // Collect fields which will participate in query
    val f = evalFields(fields).filter(_ != PRIMARY_KEY)
    // Prepare and execute a query
    val r = relation AS "root"
    val q = r.criteria.add(r.PRIMARY_KEY EQ PRIMARY_KEY()).mkUpdate()
    f.foreach(f => q.SET[Any](f.asInstanceOf[Field[Any, R]], f.value))
    val result = q.execute()
    if (relation.isAutoRefresh) refresh()
    // Invalidate caches
    relation.cache.put(PRIMARY_KEY().toString, this)
    relation.associations.foreach(_.inverseCache.invalidate())
    // Execute events
    relation.afterUpdate.foreach(c => c(this))
    result
  }

  def UPDATE(fields: Field[_, R]*): Int = {
    validate_!()
    UPDATE_!(fields: _*)
  }

  def DELETE_!(): Int = if (relation.isReadOnly)
    throw new ORMException("The relation " + relation.qualifiedName + " is read-only.")
  else {
    if (PRIMARY_KEY.isEmpty)
      throw new ORMException("Delete is only allowed with non-null PRIMARY KEY field.")
    // Execute events
    relation.beforeDelete.foreach(c => c(this))
    // Prepare and execute query
    val result = (relation AS "root")
        .map(r => r.criteria.add(r.PRIMARY_KEY EQ PRIMARY_KEY())).mkDelete().execute()
    // Invalidate caches
    relation.cache.evict(PRIMARY_KEY().toString)
    relation.associations.foreach(_.inverseCache.invalidate())
    // Execute events
    relation.afterDelete.foreach(c => c(this))
    result
  }

  /*! The `validate` method runs record validation and returns a sequence
  of error messages in case it fails. Its `validate_!` counterpart throws
  an exception in case of validation failure or just exits silently.
  */
  def validate(): Option[Seq[Msg]] = {
    val errors = relation.validation.validate(this)
    if (errors.size <= 0) None
    else Some(List(errors: _*))
  }

  def validate_!() {
    validate().map(errors => throw new ValidationException(errors))
  }

  /*! The `save` method (and its non-validating counterpart `save_!`)
  just issue `INSERT` on transient records and `UPDATE` on persistent ones.
  */
  def save_!(): Int = if (isTransient)
    throw new ORMException("Application-assigned identifier is expected. " +
        "Use one of the generators if you wish identifiers to be generated automatically.")
  else relation.get(PRIMARY_KEY()) match {
    case Some(_) => UPDATE_!()
    case _ => INSERT_!()
  }

  def save(): Int = {
    validate_!()
    save_!()
  }

  // Internal helpers

  protected def evalFields(fields: Seq[Field[_, R]]): Seq[Field[_, R]] =
    (if (fields.size == 0) relation.fields else fields)
        .map(f => relation.getField(this, f))

  /*!## Field Compositions
  
  Fields can be grouped into field compositions using the `composition` method.
  Compositions can be used as primary keys and participate in simple queries.
  Circumflex ORM currently supports only composition with arity of 2. A pair
  of fields is implicitly converted into `FieldComposition2` when necessary.
  */
  def composition[T1, T2](f1: Field[T1, R], f2: Field[T2, R]) =
    new FieldComposition2[T1, T2, R](f1, f2, this)

  /*!## Inverse Associations

  One-to-one and one-to-many relationships can be implemented using `inverseOne`
  or `inverseMany` methods.
  */
  def inverseOne[C <: Record[_, C]](association: Association[PK, C, R]) =
    new InverseOne[PK, C, R](this, association)

  def inverseMany[C <: Record[_, C]](association: Association[PK, C, R]) =
    new InverseMany[PK, C, R](this, association)

  /*!## Equality & Others
  
  Two record are considered equal if they share the same type and have same primary keys.
  Transient records are never equal to each other.

  The `hashCode` method delegates to record's primary key.

  The `canEqual` method indicates that two records share the same type.

  Finally, the default implementation of `toString` returns fully-qualified class name
  of the record followed by "@" and it's primary key value (or `TRANSIENT` word if
  primary key is `null`).
  */
  override def equals(that: Any) = that match {
    case that: Record[_, _] =>
      !(this.PRIMARY_KEY.isEmpty || that.PRIMARY_KEY.isEmpty) &&
          this.PRIMARY_KEY.value == that.PRIMARY_KEY.value &&
          this.getClass == that.getClass
    case _ => false
  }

  override def hashCode = PRIMARY_KEY.hashCode

  override def canEqual(that: Any): Boolean = that match {
    case that: Relation[_, _] =>
      this.getClass == that.recordClass
    case that: Record[_, _] =>
      this.getClass == that.getClass
    case _ => false
  }

  override def toString = getClass.getSimpleName + "@" +
      PRIMARY_KEY.map(_.toString).getOrElse("TRANSIENT")

}

/*!# Identity Generation Strategies

Different identity generation strategies can be used by mixing in one of the
`Generator` traits. Following identity generators are supported out-of-box:

  * application-assigned identifiers (the default one, no need to mixin traits):
    application is responsible for generating and assigning identifiers before
    attempting to persist a record;

  * `IdentityGenerator` is a database-specific strategy: application should
    persist a record with `NULL` primary key value, database is responsible
    for generating an identifier value and for exposing last generated identifier;

  * `SequenceGenerator` assumes that database supports sequences: the database
    is polled for next sequence value which is then used as an identifier
    for persisting.
*/
trait Generator[PK, R <: Record[PK, R]] extends Record[PK, R] { this: R =>

  override protected def _persist(fields: scala.Seq[Field[_, R]]): Int =
    persist(fields)

  def persist(fields: Seq[Field[_, R]]): Int

  override def save_!(): Int = if (isTransient) INSERT_!() else UPDATE_!()

}

trait IdentityGenerator[PK, R <: Record[PK, R]] extends Generator[PK, R] { this: R =>

  def persist(fields: scala.Seq[Field[_, R]]): Int = {
    // Make sure that PRIMARY_KEY contains `NULL`
    this.PRIMARY_KEY.setNull()
    // Persist all not-null fields
    val result = new Insert(relation, fields.filter(!_.isEmpty)).execute()
    // Fetch either the whole record or just an identifier
    val root = relation.AS("root")
    if (relation.isAutoRefresh)
      SELECT(root.*)
          .FROM(root)
          .WHERE(ormConf.dialect.identityLastIdPredicate(root))
          .unique() match {
        case Some(r: R) => relation.copyFields(r, this)
        case _ =>
          throw new ORMException("Backend didn't return last inserted record. " +
              "Try another identifier generation strategy.")
      }
    else ormConf.dialect.identityLastIdQuery(root).unique() match {
      case Some(id: PK) => this.PRIMARY_KEY := id
      case _ =>
        throw new ORMException("Backend didn't return last generated identity. " +
            "Try another identifier generation strategy.")
    }
    result
  }

}

trait SequenceGenerator[PK, R <: Record[PK, R]] extends Generator[PK, R] { this: R =>

  def persist(fields: scala.Seq[Field[_, R]]): Int = {
    // Poll database for next sequence value
    val root = relation.AS("root")
    ormConf.dialect.sequenceNextValQuery(root).unique() match {
      case Some(id: PK) =>
        // Assign retrieved id and persist all not-null fields
        val f = fields.map { f =>
          if (f == this.PRIMARY_KEY)
            f.asInstanceOf[Field[PK, R]].set(Some(id))
          f
        }.filter(!_.isEmpty)
        val result = new Insert(relation, f).execute()
        // Perform additional select if required
        if (relation.isAutoRefresh)
          refresh()
        result
      case _ =>
        throw new ORMException("Backend didn't return next sequence value. " +
            "Try another identifier generation strategy.")
    }
  }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy