Scala+DBMS+Web スカラ座の夜

2011年9月11日

ActiveRecordのようにDB操作 複数レコードの扱い

Filed under: Scala — admin @ 6:25 PM

BeanTalkクラスを前回紹介しました。
ActiveRecordに近づけるために、find関数で複数レコードを扱えるように機能拡張しました。



クラス名をBeansTalkと複数形にしました。
bean stalkと単語の区切りを変えると、豆の木(茎)になり「ジャックと豆の木」という意味になることに気が付きました。



ActiveRecordについていろいろなことを書きましたが、正直、Ruby on railsのActiveRecordでコードを書いたことがないので、
頓珍漢なことを書いていてもActiveRecordフリークの方々ご容赦ください。



とわいえ、ActiveRecordfind_by_sqlの逆を行くわけで、長年SQL文を書いて生活の糧を得ていた者としては真逆の行為です。

しかし、簡単なWEBアプリケーションを書くなら、これで十分という気持ちです。

複数レコードを扱えるようにしたので、N:Nの関係を表現できます。



BlogやSNSを実装するならこれで十分かな?


package seedo.database
import scala.collection.mutable.{HashMap,LinkedHashMap,HashSet,ArrayBuffer}
import scala.reflect._
import scala.collection.mutable.Map
import seedo._

class BeansTalk[T] (implicit m : ClassManifest[T]) {
	var beanObj = m.erasure.newInstance().asInstanceOf[T]	// TのclassのBean を生成
	var joinEntity = Map.empty[String,Array[AnyRef]]	// joinしているテーブルのレコード

	val beanInfo = new FromBean(beanObj.asInstanceOf[AnyRef])	// Beanのアノテーション情報からテーブル情報を読み取る
	var praimaryKey : String = null	// プライマリキー
	if(beanInfo.praimaryKey.length == 1)
		praimaryKey = beanInfo.praimaryKey(0)
	var beanObjs:Array[T] = null	// 複数レコードが検索されたときに格納
	var joinEntities:Array[Map[String,Array[AnyRef]]] = null	// 複数レコードのときにJOINした表データを格納
	var column:String = "*"		// SELECTするときに読み込むカラムを指定
	var key:String = null		// Order by句 のソートキー
	var condition:String = null	// Where句の条件
	var values:Array[Any] = null	// 条件に指定する値
	// SELECT文の構成要素を初期化
	def clean:Unit = {
		column = "*"
		key = null
		condition = null
		values = null
	}
	var db:Db = null	// Dbクラス
	var tran = true		// トランザクション制御する。外部からDbクラスのオブジェクトが渡されたときはトランザクションモード(false)
	def bean : T = beanObj
	def entity : T = beanObj
	def beans : Array[T] = beanObjs
	var joins = Map.empty[String,List[List[String]]]	// join先のBean名とjoinの条件
	/**
	 * Joinしているテーブルのレコードを返す
	 */
	def getjoinEntity(joinBeanName:String) :Option[Array[AnyRef]] = {
	  	joinEntity.get(joinBeanName)
	}
	// join has_one / has_many
	def addJoin(beanName:String,on:List[String]*) : Unit = {
	  joins.put(beanName,on.toList)
	}
	def removeJoin(beanName:String) : Unit = {
	  joins.remove(beanName)
	  joinEntity.remove(beanName)
	}
	def removeAllJoin() : Unit = {
	  joins.clear
	  joinEntity.clear
	}
	def setDb(dbx:Db):Unit = {
		db = dbx
		tran = false
	} 
	// SELECT One record. praimaryKey is some columns.
	def getEntity(pk:Any*) : T = {
	  if(tran)
		  db = new Db
	  if(column==null)
		column = "*"
	  var sql:DL = db
	  val result = db.selectBean(beanObj.asInstanceOf[AnyRef],pk:_*)
	  if(tran){
		  db.close
		  db = null
	  }
	  joinsRecords(beanObj, joinEntity)
	  clean
	  return result.asInstanceOf[T]
	}
	// set bean object.
	def setEntity(e:T) : Unit = {
		beanObj = e
	}
	// select One record
	def find : T = {
	  if(tran)
		  db = new Db
	  if(column==null)
		  column = "*"
	  val sql:DL = db
	  val result = db.selectBean(beanObj.asInstanceOf[AnyRef])
	  if(tran){
		  db.close
		  db = null
	  }
	  joinsRecords(beanObj, joinEntity)
	  clean
	  return result.asInstanceOf[T]
	}
	// gen 'in' sentence.
	private def qu(no:Int):String = {
		var str = ""
		for(i <- 1 to no){
			if(i != 1){
			  str += ","
			}
			str += "?"
		}
		str
	}
	// select join records.
	private def joinsRecords(beanO:T,joinE:Map[String,Array[AnyRef]]) :Unit = {
	  joinE.clear
	  joins.foreach(j => {
		  val clazz = beanO.getClass
		  val k = j._2
		  var whereStr = ""
		  val whereVal = new ArrayBuffer[Any]
		  var columnStr = "*"
		  var n = 0
		  k.foreach(i => {
			  if(i.length > 2){
				  val methodName = "get" + i(0)	// Master側のJOINカラム BENAからそのカラムの値を取り出す
				  val v = clazz.getMethod(methodName).invoke(beanO)
				  whereVal += v
				  if(n != 0){
					  whereStr += " and "
				  }
				  whereStr += i(2) + i(1) + "?"	// Detail側のJOINカラム 抽出条件を作る。i(2)は、JOIN側のカラム名、i(1)は比較演算子「'=','<>','<','>','<=','>='」
				  
				  if(i.length > 3){
					columnStr = i(3)	// Detail側のSELECTしたときに取るカラム
				  }
			  }else{
				  throw new Exception("Join sentence should be 'Master.column name' + '=' + 'Detail.column name'. <- " + i.toString)
			  }
			  n += 1
		  })
		  val dl = db.select(columnStr)
		  if(whereVal.length > 0){
			  dl.whereParam(whereStr, whereVal.toArray)
		  }
		  val result = db.executeQueryBean(j._1)
		  joinE.put(j._1,result.toArray)
	  })	  
	}
	// SELECT some records. praimaryKey is one column.
	def find(ids:Any*) : Array[T] = {
	  joinEntities = null
	  if(tran)
		  db = new Db
	  if(column==null)
		  column = "*"
	  var sql:DL = db.select(column).from(beanInfo.table)
	  if(ids != null) {
		  sql.where(praimaryKey + " in (" + qu(ids.length) + ")",ids:_*)
	  }
	  if(key != null) {
		  sql.orderBy(key)
	  }
	  val beanObjsBuf = db.executeQuery(beanObj.asInstanceOf[AnyRef]).asInstanceOf[ArrayBuffer[T]]
	  if(beanObjsBuf.length == 1){	// 検索結果が1の場合は、自身のbeanObjを入れ替える
		  beanObj = beanObjsBuf(0)
		  joinsRecords(beanObj, joinEntity)
	  } else {
		  joinEntities = new Array[Map[String,Array[AnyRef]]](beanObjsBuf.length)
		  var i = 0
		  beanObjsBuf.foreach(v => {
			  val je = Map.empty[String,Array[AnyRef]]
			  joinsRecords(v, je)
			  joinEntities(i) = je
			  i += 1
		  })
	  }
	  if(tran){
		  db.close
		  db = null
	  }
	  clean
	  beanObjs = beanObjsBuf.toArray 
	  return beanObjs
	}
	// SELECT WHERE句で条件をレコードを絞り込む
	def findAll : Array[T] = {
	  if(tran)
		  db = new Db
	  var sql:DL = db.select(column).from(beanInfo.table)
	  if(condition != null){
		  sql.whereParam(condition,values)
	  }
	  if(key != null) {
		  sql.orderBy(key)
	  }
	  val beanObjsBuf = db.executeQuery(beanObj.asInstanceOf[AnyRef]).asInstanceOf[ArrayBuffer[T]]
	  if(tran){
		  db.close
		  db = null
	  }
	  clean
	  beanObjs = beanObjsBuf.toArray 
	  return beanObjs
	}
	// UPDATE one record
	def update : Int = {
		if(tran)
			db = new Db
		try {
			return db.updateBean(beanObj.asInstanceOf[AnyRef])
		} catch {
			case e:Exception => {
				db.rollback
				throw e
			}
		} finally {
			if(tran){
				db.commit
				db.close
				db = null
			}
			clean
		}
	}
	def save : Int = {
		update
	}
	// DELETE one record
	def delete : Int = {
		if(tran)
			db = new Db
		try {
			return db.deleteBean(beanObj.asInstanceOf[AnyRef])
		} catch {
			case e:Exception => {
				db.rollback
				throw e
			}
		} finally {
			if(tran){
				db.commit
				db.close
				db = null
			}
			clean
		}
	}
	// DELET some records. praimaryKey is one column.
	def delete(ids:Any*) : Int = {
		if(tran)
			db = new Db
		try {
			var sql:DL = db.deleteFrom(beanInfo.table)
			if(ids != null) {
				sql.where(praimaryKey + " in (" + qu(ids.length) + ")",ids:_*)
			}
			return db.execute.getResultSize
		} catch {
			case e:Exception => {
				db.rollback
				throw e
			}
		} finally {
			if(tran){
				db.commit
				db.close
				db = null
			}
			clean
	  }
	}
	// INSERT one record
	def create : Int = {
		if(tran)
			db = new Db
		try {
			return db.insertBean(beanObj.asInstanceOf[AnyRef])
		} catch {
			case e:Exception => {
				db.rollback
				throw e
			}
		} finally {
			if(tran){
				db.commit
				db.close
				db = null
			}
			clean
		}
	}
	// SQL SELECT sentence
	def column(col:String) :BeansTalk[T] = {
		this.column = col
		this
	}
	def where(condition:String,values:Any*) :BeansTalk[T] = {
		this.condition = condition
		this.values = values.toArray
		this
	}
	def conditions(condition:String,values:Any*) :BeansTalk[T] = {
		this.condition = condition
		this.values = values.toArray
		this
	}
	def orderBy(k:String) :BeansTalk[T] = {
		this.key = k
		this
	}
	def commit : BeansTalk[T] = {
		if(!tran)
			db.commit
		this
	}
	def rollback : BeansTalk[T] = {
		if(!tran)
			db.rollback
		this
	}
	def dump : Unit = {
		println(beanObj.toString)
		if(joinEntity != null) {
			joinEntity.foreach(w => {
				println(" " + w._1)
				w._2.foreach(x =>{
					println("  " + x)
				})
			})
		}
	}
	def dumps : Unit = {
		var i = 0
		if(beanObjs == null)
			return
		beanObjs.foreach(r =>{
			println(r.toString)
			if(joinEntities != null) {
				joinEntities(i).foreach(w =>{
					println(" " + w._1)
					w._2.foreach(x =>{
						println("  " + x)
					})
				})
			}
			i += 1
		})
	}
}




テストコードです



find関数に従業員IDを3つの引数でを渡します。

@Test def test1 {
	println("--- TSET 1 ---")
	beanobj.addJoin("scott.bean.DEPT",List("DEPTNO","=","DEPTNO"))
	val r = beanobj.find(7788,7698,7934)
	beanobj.dumps
}




実行結果


--- TSET 1 ---
7698,BLAKE,MANAGER,7839,1981-05-01 00:00:00.0,2850,,30
 scott.bean.DEPT
  30,SALES,CHICAGO
7788,SCOTT,ANALYST,7566,1987-04-19 00:00:00.0,3000,,20
 scott.bean.DEPT
  20,RESEARCH,DALLAS
7934,MILLER,CLERK,7782,1982-01-23 00:00:00.0,1300,,10
 scott.bean.DEPT
  10,ACCOUNTING,NEW YORK
--- End TEST ---




テストコードその2



テスト2-1では、
where関数で検索する条件としてENAMEがSで始まる人をSELECTして、
orderBy関数によってENAMEの順序でソートします。

テスト2-2では、
where関数により給与が1500以上の条件で、
column関数でSELECTするカラムを”empno,sal,ename”と指定します。
orderBy関数によって給与順にソート。

@Test def test2 {
	println("--- TSET 2-1 ---")
	val r1 = beanobj.where("ename like ?","S%").orderBy("ename").findAll
	r1.foreach(v => println(v.toString))
	println("--- TSET 2-2 ---")
	val r2 = beanobj.column("empno,sal,ename").where("sal > ?",1500).orderBy("sal").findAll
	r2.foreach(v => println(v.toString))
}




実行結果


--- TSET 2-1 ---
7788,SCOTT,ANALYST,7566,1987-04-19 00:00:00.0,3000,,20
7369,SMITH,CLERK,7902,1980-12-17 00:00:00.0,800,,20


--- TSET 2-2 ---
7499,ALLEN,1600
7782,CLARK,2450
7698,BLAKE,2850
7566,JONES,2975
7788,SCOTT,3000
7902,FORD,3000
7839,KING,5000
--- End TEST ---




BeansTalkクラスは、seedo-1.2.4.jarに格納しました。
動かしてみたい方は、
seedo-1.2.4.jarはこのリンクからダウンロードできます。

BEANを使うために以下の2つのJARもダウンロードしてください。
scott.jar と jpa.jar

MySQLの環境は事前に作ってください。

コメントはまだありません »

No comments yet.

RSS feed for comments on this post. TrackBack URL

Leave a comment

コメントを投稿するにはログインしてください。

Powered by WordPress