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

.circumflex-orm.1.2.source-code.record.scala Maven / Gradle / Ivy

package ru.circumflex.orm

import ORM._
import JDBC._

// ## Record

/**
 * *Record* is a cornerstone of relational model. In general, each instance of
 * pesistent class is stored as a single record in a relation corresponding to that
 * class. Since Circumflex ORM employs the Active Relation design approach to
 * persistence, each persistent class should subclass `Record`.
 */
abstract class Record[R <: Record[R]] { this: R =>

  // ### Implicits

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

  // ### Commons

  /**
   * Unique identifier based on fully-qualified class name of
   * this record, which is used to uniquely identify this record
   * class among others.
   */
  val uuid = getClass.getName

  /**
   * A `Relation[R]` corresponding to this record.
   *
   * In general the relations should be the companion objects of records:
   *
   *     class Country extends Record[Country]  {
   *       val name = TEXT NOT_NULL
   * }
   *
   *     object Country extends Table[Country]
   *
   * However, if you prefer different naming conventions, you should override
   * this method.
   */
  def relation = RelationRegistry.getRelation(this)

  /**
   * We only support auto-generated `BIGINT` columns as primary keys
   * for a couple of reasons. Sorry.
   */
  val id = new PrimaryKeyField(this)

  /**
   * Yield `true` if `primaryKey` field is empty (contains `None`).
   */
  def transient_?(): Boolean = id.get() == None

  /**
   * Non-DSL field creation.
   */
  def field[T](name: String, sqlType: String) =
    new Field[T](name, uuid + "." + name, sqlType)

  /**
   * Inverse associations.
   */
  def inverse[C <: Record[C]](association: Association[C, R]): InverseAssociation[R, C] =
    new InverseAssociation(this, association)

  // ### Validate, Insert, Update, Save and Delete

  /**
   * Performs record validation.
   */
  def validate(): Option[ValidationErrors] = {
    val errors = relation.validation.validate(this)
    if (errors.size == 0) None
    else Some(new ValidationErrors(errors: _*))
  }

  /**
   * Perfoms record validation and throws `ValidationException` instead of peacefully returning
   * `ValidationError`s.
   */
  def validate_!(): Unit = validate match {
    case Some(errors) => throw errors.toException
    case _ =>
  }

  /**
   * Skips the validation and performs `INSERT` statement for this record.
   * If no `fields` specified, performs full insert (except empty fields
   * with default values), otherwise only specified `fields` participate
   * in the statement.
   */
  def insert_!(fields: Field[_]*): Int = if (relation.readOnly_?)
    throw new ORMException("The relation " + relation.qualifiedName + " is read-only.")
  else transactionManager.dml(conn => {
    // Execute events
    relation.beforeInsert.foreach(c => c(this))
    // Collect fields which will participate in query
    var f: Seq[Field[_]] = if (fields.size == 0) _fields.filter(f => !f.empty_?) else fields
    // Prepare and execute query
    val sql = dialect.insertRecord(this, f)
    sqlLog.debug(sql)
    val result = auto(conn.prepareStatement(sql))(st => {
      relation.setParams(this, st, f)
      st.executeUpdate
    })
    // Issue additional select to read generated ID (and default column values)
    relation.refetchLast(this)
    // Execute events
    relation.afterInsert.foreach(c => c(this))
    return result
  })
  def INSERT_!(fields: Field[_]*): Int = insert_!(fields: _*)

  /**
   * Validates record and executes `insert_!` on success.
   */
  def insert(fields: Field[_]*): Int = {
    validate_!()
    insert_!(fields: _*)
  }
  def INSERT(fields: Field[_]*) = insert(fields: _*)

  /**
   * Skips the validation and performs `UPDATE` statement for this record.
   * If no `fields` specified, performs full update, otherwise only specified
   * `fields` participate in the statement.
   */
  def update_!(fields: Field[_]*): Int = if (relation.readOnly_?)
    throw new ORMException("The relation " + relation.qualifiedName + " is read-only.")
  else transactionManager.dml(conn => {
    // Execute events
    relation.beforeUpdate.foreach(c => c(this))
    // Collect fields which will participate in query
    val f: Seq[Field[_]] = if (fields.size == 0) _fields.filter(f => f != id) else fields
    // Prepare and execute a query
    val sql = dialect.updateRecord(this, f)
    sqlLog.debug(sql)
    val result = auto(conn.prepareStatement(sql))(st => {
      relation.setParams(this, st, f)
      typeConverter.write(st, id.getValue, f.size + 1)
      st.executeUpdate
    })
    // Execute events
    relation.afterUpdate.foreach(c => c(this))
    return result
  })
  def UPDATE_!(fields: Field[_]*): Int = update_!(fields: _*)

  /**
   * Validates record and executes `update_!` on success.
   */
  def update(fields: Field[_]*): Int = {
    validate_!()
    update_!(fields: _*)
  }
  def UPDATE(fields: Field[_]*) = update(fields: _*)

  /**
   * Executes the `DELETE` statement for this record using primary key
   * as delete criteria.
   */
  def delete_!(): Int = if (relation.readOnly_?)
    throw new ORMException("The relation " + relation.qualifiedName + " is read-only.")
  else transactionManager.dml(conn => {
    // Execute events
    relation.beforeDelete.foreach(c => c(this))
    // Prepare and execute query
    val sql = dialect.deleteRecord(this)
    sqlLog.debug(sql)
    val result = auto(conn.prepareStatement(sql))(st => {
      typeConverter.write(st, id.getValue, 1)
      st.executeUpdate
    })
    // Execute events
    relation.afterDelete.foreach(c => c(this))
    return result
  })
  def DELETE_!(): Int = delete_!()

  /**
   * If record's `id` field is not `NULL` perform `update`, otherwise perform `insert`
   * and then refetch record using last generated identity.
   */
  def save_!(): Int = if (transient_?) insert_!() else update_!()

  /**
   * Validates record and executes `save_!` on success.
   */
  def save(): Int = {
    validate_!()
    save_!()
  }

  /**
   * Invalidates transaction-scoped cache for this record and refetches it from database.
   */
  def refresh(): this.type = if (transient_?)
    throw new ORMException("Could not refresh transient record.")
  else {
    tx.evictRecordCache(this)
    val root = relation.as("root")
    val id = this.id()
    SELECT (root.*) FROM root WHERE (root.id EQ id) unique match {
      case Some(r: R) => relation.copyFields(r, this)
      case _ =>
        throw new ORMException("Could not locate record with id = " + id + " in database.")
    }
    return this
  }

  // ### Miscellaneous

  /**
   * Get all fields of current record (involves some reflection).
   */
  protected[orm] lazy val _fields: Seq[Field[_]] = relation.fields
      .map(f => relation.methodsMap(f).invoke(this) match {
    case f: Field[_] => f
    case a: Association[_, _] => a.field
    case m => throw new ORMException("Unknown member: " + m)
  })

  /**
   * Set a specified `value` to specified `holder`.
   */
  def setValue(vh: ValueHolder[_], value: Any): Unit = value match {
    case Some(value) => setValue(vh, value)
    case None => setValue(vh, null)
    case _ => vh match {
      case f: Field[Any] => f.setValue(value)
      case a: Association[_, _] => value match {
        case id: Long => setValue(a.field, id)
        case rec: Record[_] => setValue(a.field, rec.id.getValue)
        case _ => throw new ORMException("Could not set value " + value +
            " to association " + a + ".")
      }
      case _ => throw new ORMException("Could not set value " + value +
          " to specified value holder " + vh + ".")
    }
  }

  /**
   * Search for specified `field` among this record methods and set it's `value`.
   */
  def setField[T](field: Field[T], value: T): Unit = _fields.find(f => f == field) match {
    case Some(f: Field[T]) => f.setValue(value)
    case _ =>
  }

  override def equals(obj: Any) = obj match {
    case r: R => this.id() == r.id()
    case _ => false
  }

  override def hashCode = id.getOrElse(0l).hashCode

  override def toString = getClass.getSimpleName + "@" + id.toString("TRANSIENT")

}

// ## Helper for fields DSL

/**
 * A tiny builder that helps to instantiate `Field`s and `Association`s
 * in a neat DSL-like way.
 */
class DefinitionHelper[R <: Record[R]](record: R, name: String) {

  def uuid = record.uuid + "." + name

  def integer = new IntField(name, uuid)
  def bigint = new LongField(name, uuid)
  def numeric(precision: Int = -1, scale: Int = 0) = new NumericField(name, uuid, precision, scale)
  def text = new TextField(name, uuid, dialect.textType)
  def varchar(length: Int = -1) = new TextField(name, uuid, length)
  def boolean = new BooleanField(name, uuid)
  def date = new DateField(name, uuid)
  def time = new TimeField(name, uuid)
  def timestamp = new TimestampField(name, uuid)
  def xml = new XmlField(name, uuid)

  def INTEGER = integer
  def BIGINT = bigint
  def NUMERIC(precision: Int = -1, scale: Int = 1) = numeric(precision, scale)
  def TEXT = text
  def VARCHAR(length: Int = -1) = varchar(length)
  def BOOLEAN = boolean
  def DATE = date
  def TIME = time
  def TIMESTAMP = timestamp
  def XML = xml

  def references[F <: Record[F]](relation: Relation[F]): Association[R, F] =
    new Association[R, F](name, uuid, record, relation)
  def REFERENCES[F <: Record[F]](relation: Relation[F]): Association[R, F] =
    references(relation)
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy