Forum OpenACS Development: Re: OpenACS, XoTcl, and ActiveRecord

Collapse
Posted by Gustaf Neumann on
a couple of issues are mentioned above, where i start
with the short answers, such people can skip the long answers
below:

Lars first question: i think, you questions concerning
the class name and the colon handling are sorted out.
A simple thing, to make things a little more pretty is
to define an instproc name available for all objects
(and classes) such as
  Object instproc name {} {return [namespace tail [self]]}
then you can do a ... [my name] in each object or class.

---

Guan: keeping business logic in one class: you can
keep things certainly "in one class",

  ActiveRecord Person -parameter {{credit 0}}
  Person instproc email_addr {} {
  return [namespace tail [mailto:self]]@somehost.somedomain
  }
  Person instproc send_email {subject message} {
    puts "send_email_proc [my email_addr] $subject $message"
  }

  Person jeff
  jeff send_email "hi" "some text"

The only separation here is that xotcl/tcl uses separate
commands to define methods/procs which can be incrementally
redefined. If you prefer a more close appearance, one
can use

  ActiveRecord Person -parameter {{credit 0}} \
    -proc table_name {} { return "people" } \
    -proc id_column {} { return "people_id" } \
    -instproc email_addr {} {
      return [namespace tail [mailto:self]]@somehost.somedomain
    } \
    -instproc send_email {subject msg} {
      puts "send_email_proc [my email_addr] $subject $msg"
    }

since one can call every method via the configure call
in the creation.

---

Concering the active record example: Lars, you
might find some usage of the following code, that
i posting here, since it is not very long. I wrote
this code two years ago, which follows the idea that
one creates classes/object in xotcl and xotcl
tries to create query and update the database underneath.

This might not the best thing for usage in openacs,
where you probably want more control over the database,
use stored procedures and such, but anyhow, one can
use it as a source for ideas and code snipplets. One
can subclass the provided methods to make use
of faster queries and use the generic ones as default.
When i remember correctly, neophytos did some experiences
with the code below, and came most probably up with
something better and more clever than this little demo....

I will add this to the xotcl distro and put it
under the tcl-style open license.

Basically,
- one creates classes and objects,
- during the creation of classes, the system
  checks for the existance of corresponding tables and
  creates it on the fly if necessary;
- on the creation of objects, it checks for
  the existance of tuples and fetches it if possible.
- The system uses write traces for variables to
  maintain a dirty flag;
- when the flag is set, an update call updates the
  database;
- when the object is destroyed, it performs the
  update as well.

Here is a sample file. the DB_Manager defines
the linkage with the database. DB_Classes are
Classes with persistent data in the database.
The database attributes are defined via '-att',
which are embedded objects that might contain
more metadata (such the name of the attributes,
its datatype, wether it is a primary key or not, etc.)
For fun, i defined two different mapping strategies,
relational and object-relational, where the latter
one is using postgres' object relational definitions.

----
package require xotcl::store::reldb
package require xotcl::store::postgres

DB_Manager dbm1
DB_Table parameter {{dbmgr dbm1}}

DB_Class Person -superclass DB_Object \
    -att {id -attname PERSON_ID -pk 1 -type integer} \
    -att {count -type integer} \
    -att age \
    -att name
DB_Class Employee -superclass Person \
    -mapping object-relational \
    -att ssn

DB_Class ProjectLeader -superclass Employee -table LEADER \
    -mapping object-relational \
    -att {project -attname PROJECT_ID -type integer}

DB_Class Project -superclass DB_Object \
    -att {id  -attname PROJECT_ID -pk 1 -type integer} \
    -att name \
    -att {mgr  -attname MANAGER}

DB_Class DevelopmentProject -superclass Project -table DEVPROJECT \
    -att {product -attname PRODUCT_ID -type integer}

Person p -id 111 -age 22 -name "Gustaf" -count 0
Employee e1 -id 4711
ProjectLeader pp -id 333
Person p1 -id 111

p incr count

DevelopmentProject openacs -id 123 -name OpenACS -mgr nobody -product 222

--------------

The db mapping is defined below.
It contains the definition for

- DB_Att (database attributes)
  These hold metadata about attributes, such as
  name of the attribute in the database, data type,
  the name of the corresponding variable in the
  xotcl object, etc.)

- DB_Table (class for maintaining tables)
  for the tables, subobjects are defined on the
  class level (e.g. Person::table), which do all
  perform the datbase logic; the tables contain
  as subobjects instances of DB_ATT, which
  contain the meta data about the attributes
  of the table (e.g. Person::table::age)
  It has methods for defining e.q.
  SQL create, select or insert statements.

- DB_Class (similar do ::xotcl::Class):
  a meta class providing the linkage
  between application classes and DB_Tables.

- DB_Mapping (the two experimental mappings)

- DB_Object (similar to ::xotcl::Object), containing
  methods avaiable to all objects with persistant
  data.

--------------
package provide xotcl::store::reldb 0.9

Class DB_Att  -parameter {
  {type varchar(255)}
  {attname {[namespace tail [self]]}}
  {pk 0}
}
DB_Att instproc varname {} {
  return [namespace tail [self]]
}
DB_Att instproc quoted {value} {
  if {[my type] == "integer"} { return $value }
  return '$value'
}
DB_Att instproc = o {
  return [my attname]=[my quoted [$o set [my varname]]]
}

Class DB_Table -parameter {table inherits references}
DB_Table instproc allAtts {} {
  set atts [my info children]
  if {[my exists inherits]} {
    set atts [concat $atts [[my inherits] allAtts]]
  }
  return $atts
}
DB_Table instproc pk {} {
  foreach a [my info children] {if {[$a pk]} { return $a }}
  if {[my exists inherits]} {return [[my set inherits] pk]}
  if {[my exists references]} {return [[my set references] pk]}
  return ""
}

DB_Table instproc Select {o} {
  my instvar dbmgr
  set dbatts [list]
  set vars [list]
  foreach a [my allAtts] {
    lappend dbatts [$a attname]
    lappend vars [$a varname]
  }
  if {[my exists references]} {
    set r [my references]
    #set rpk [$r pk]
    #lappend dbatts [$rpk attname]
    #lappend vars [$rpk varname]
    $r Select $o
  }
  set cmd "SELECT [join $dbatts ,] FROM [my table] WHERE [[my pk] = $o]"
  return [$dbmgr fetch "$cmd;" $o $vars]
}
DB_Table instproc Insert {o} {
  my instvar dbmgr
  set dbatts [list]
  set values [list]
  foreach a [my allAtts] {
    set vn [$a varname]
    if {[$o exists $vn]} {
      lappend dbatts [$a attname]
      lappend values [$a quoted [$o set $vn]]
    }
  }
  if {[my exists references]} {
    set r [my references]
    set rpk [$r pk]
    set vn [$rpk varname]
    lappend dbatts [$rpk attname]
    lappend values [$rpk quoted [$o set $vn]]
    $r Insert $o
  }
  set cmd "INSERT into [my table] ([join $dbatts {, }]) \
    VALUES ([join $values {, }])"
  return [$dbmgr doSql "$cmd;"]
}
DB_Table instproc Create {} {
  my instvar dbmgr
  if {[$dbmgr tableExists [my table]]} return
  set dbatts [list]
  foreach a [my info children] {
    set decl "[$a attname] [$a type]"
    if {[$a pk]} {append decl " PRIMARY KEY"}
    lappend dbatts $decl
  }
  set ref ""
  if {[my exists references]} {
    set r [my references]
    set a [$r pk]
    lappend dbatts "[$a attname] [$a type]"
    set ref ", FOREIGN KEY([$a attname]) REFERENCES [$r table]"
  }
  set attspec [join $dbatts ",\n\t"]
  set cmd "CREATE TABLE [my table] (\n\t$attspec$ref\n)"
  if {[my exists inherits]} {
    append cmd " INHERITS ([[my inherits] table])"
  }
  $dbmgr doSql "$cmd;"
}
DB_Table instproc Update {o} {
  my instvar dbmgr
  set pairs [list]
  foreach a [my allAtts] {
    if {[$o exists [$a varname]]} {
      lappend pairs [$a = $o]
    }
  }
  if {[my exists references]} {
    set r [my references]
    lappend pairs [[$r pk] = $o]
    $r Update $o
  }
  set cmd "UPDATE [my table] SET [join $pairs ,] WHERE [[my pk] = $o]"
  return [$dbmgr doSql "$cmd;"]
}

Class DB_Class -superclass Class -parameter {
  {mapping relational}
  {table {[namespace tail [self]]}}
}
DB_Class instproc att list {
  if {![my isobject [self]::table]} {
    DB_Table create [self]::table -table [my table]
  }
  eval DB_Att create [self]::table::$list
  my parameter [lindex $list 0]
}

DB_Class instproc init {} {
  my mixin DB_mapping=[my mapping]
  my tableLinkage
  if {[my isobject [self]::table]} {
    [self]::table Create
  }
}
DB_Class instproc update o {
  if {[$o exists __dirty_flag__]} {
    [self]::table Update $o
    $o unset __dirty_flag__
  }
}
DB_Class instproc instdestroy {o args} {
  my update $o
  set pk [[self]::table pk]
  set vn [$pk varname]
  if {[$o exists $vn]} {
    set key [$o set $vn]
    my unset keys($key)
  }
  catch {[[self]::table dbmgr] unset fetched($o)}
  next
}

DB_Class instproc create args {
  set o [next]
  set pk [[self]::table pk]
  set vn [$pk varname]
  if {[$o exists $vn]} {
    set key [$o set $vn]
    if {[my exists keys($key)]} {
      set obj [my set keys($key)]
      puts "object of class my key $key was fetched already, returning object $obj\n"
      $o destroy    ;# destroy unsets keys($key)
      return [my set keys($key) $obj]
    } else {
      if {![[self]::table Select $o]} {
    [self]::table Insert $o
      }
      my set keys($key) $o
    }
  } else {
    puts "no key specified"
  }
  return $o
}

Class DB_mapping
Class DB_mapping=relational -superclass DB_mapping
DB_mapping=relational instproc tableLinkage {} {
  set sc [my info superclass]
  if {[my isobject [self]::table] && $sc != "::DB_Object"} {
    [self]::table references ${sc}::table
  }
}

Class DB_mapping=object-relational -superclass DB_mapping
DB_mapping=object-relational instproc tableLinkage {} {
  set sc [my info superclass]
  if {[my isobject [self]::table] && $sc != "::DB_Object"} {
    [self]::table inherits ${sc}::table
  }
}

DB_Class DB_Object

DB_Object instproc vartrace {name sub op} {
  if {$op == "w"} {
    #puts "my set __dirty_flag__ 1"
    my set __dirty_flag__ 1
  }
}

-----

Below is the little postgres driver for libpgtcl
which connects to the database, queries the
table catalog and performs the real queries. The class should be replaced by something
more appropriate in openacs...

-----
package provide xotcl::store::postgres 0.9

load /usr/lib/libpgtcl.so

Class DB_Manager -parameter {
  {host localhost}
  {port 5432}
  {dbname test}
  {recreate 0}
}
DB_Manager instproc init {} {
  my instvar host port dbname conn
  set dbn [string tolower $dbname]
  set conn [pg_connect template1 -host $host -port $port]
  #set conn [pg_connect template1 -options user=postgres]
  set res [pg_exec $conn "SELECT datname FROM pg_database where datname='$dbn'"]
  set ntups [pg_result $res -numTuples]
  pg_result $res -clear
  if {$ntups == 0} {
    puts "we have to create database $dbname"
    set res [pg_exec $conn "CREATE DATABASE $dbname;"]
    pg_result $res -clear
  } else {
    pg_disconnect $conn
    puts "opening database $dbname"
    set conn [pg_connect $dbname -host $host -port $port]
  }
}
DB_Manager instproc tableExists {name} {
  my instvar conn
  if {[my recreate]} {
    set res [pg_exec $conn "DROP TABLE $name;"]
    pg_result $res -clear
    return 0
  }
  set n [string tolower $name]
  set res [pg_exec $conn \
          "SELECT tablename FROM pg_tables WHERE tablename='$n';"]
  set ntups [pg_result $res -numTuples]
  pg_result $res -clear
  return $ntups
}
DB_Manager instproc doSql {cmd} {
  my instvar conn
  set res [pg_exec $conn $cmd]
  puts "*** doSql $cmd"
  pg_result $res -clear
}
DB_Manager instproc fetch {cmd o vars} {
  my instvar conn
  set res [pg_exec $conn $cmd]
  set ntups [pg_result $res -numTuples]
  puts "$o: $cmd got $ntups tuples"
  if {$ntups > 0} {
    my set fetched($o) 1
    foreach var $vars value [pg_result $res -getTuple 0] {
      $o set $var $value
      $o trace variable $var w [list $o vartrace]
    }
  }
  pg_result $res -clear
  return $ntups
}

DB_Manager instproc disconnect {} {
  my instvar conn
  foreach o [my array names fetched] {
    [$o info class] update $o
  }
  puts "*** closing connection"
  pg_disconnect $conn
  my unset conn
}
DB_Manager instproc destroy {} {
  my disconnect
}