14. Database Access

Introduction

Making and Using a DBM File

# There are dbm, sdbm, gdbm modules
# and the bdb module for accessing the berkeley db
# sdbm seem to be available on the most systems,
# so we use it here
#
require "sdbm"
SDBM.open("filename", 0666) { |dbobj|
    # raises exception if open error
    
    # the returned sdbm-dbobj has most of the methods of a hash
    v = dbobj["key"]
    dbobj["key"] = "newvalue"
    if dbobj.has_key?("key")
        # ...
    end
    dbobj.delete("key2")
}
# database is open only inside the block.

# It is also possible to use a open .. close pair:
dbobj = SDBM.open("filename", 0666)
#.. do something with dbobj
dbobj.close

#!/usr/bin/ruby -w
# userstats - generate statistics on who is logged in
# call with usernames as argument to display the totals
# for the given usernames, call with "ALL" to display all users

require "sdbm"
filename = '/tmp/userstats.db'
SDBM.open(filename, 0666) { |dbobj|
    if ARGV.length > 0
        if ARGV[0] == "ALL"
            # ARGV is constant, so we need the variable userlist
            userlist = dbobj.keys().sort()
        else
            userlist = ARGV
        end
        userlist.each { |user|
            print "#{user}\t#{dbobj[user]}\n"
        }
    else
        who = `who`
        who.split("\n").each { |line|
            md = /^(\S+)/.match(line)
            raise "Bad line from who: #{line}" unless md
            # sdbm stores only strings, so "+=" doesn't work,
            # we need to convert them expicitly back to integer.
            if dbobj.has_key?(md[0])
                dbobj[md[0]] = dbobj[md[0]].to_i + 1
            else
                dbobj[md[0]] = "1"
            end
        }
    end
}

Emptying a DBM File

# using open and clear
dbobj = SDBM.open("filename", 0666)
dbobj.clear()
dbobj.close()
# deleting file and recreating it
# the filenames depend on the flavor of dbm you use,
# for example sdbm has two files named filename.pag and filename.dir,
# so you need to delete both files
begin
    File.delete("filename")
    # raises Exception if not exist
    dbobj = SDBM.open("filename", 0666)
rescue
    # add error handling here
end

Converting Between DBM Files

# sdbm2gdbm: converts sdbm database to a gdbm database
require "sdbm"
require "gdbm"

unless ARGV.length == 2
    fail "usage: sdbm2gdbm infile outfile"
end
infile = ARGV[0]
outfile = ARGV[1]

sdb = SDBM.open(infile)
gdb = GDBM.open(outfile, 0666)
sdb.each { |key, val|
    gdb[key] = val
}
gdb.close
sdb.close

Merging DBM Files

#!/usr/bin/ruby -w
# dbmmerge: merges two dbm databases
require "sdbm"

unless ARGV.length == 3
    fail "usage: dbmmerge indb1 indb2 outdb"
end
infile1 = ARGV[0]
infile2 = ARGV[0]
outfile = ARGV[2]

in1 = SDBM.open(infile1, nil)
in2 = SDBM.open(infile2, nil)
outdb = SDBM.open(outfile, 0666)

[in1, in2].each { |indb|
    indb.each { |key, val|
        if outdb.has_key?(key)
            # decide which value to set.
            # set outdb[key] if necessary
        else
            outdb[key] = val
        end
    }
}
in1.close
in2.close
outdb.close

Locking DBM Files

Sorting Large DBM Files

Treating a Text File as a Database Array

# we write a tie method that extends the Array class.
# It reads the file into the memory, executes the code block
# in which you can manipulate the array as needed, and writes
# the array back to the file after the end of the block execution
class Array
    def tie(filename, flags)
        File.open(filename, flags) { |f|
            f.each_line { |line|
                self.push(line.chomp)
            }
            yield
            f.rewind
            each { |line|
                if line
                    f.puts(line)
                else
                    f.puts ""
                end
            }
        }
    end
end

array = Array.new
array.tie("/tmp/textfile.txt", File::RDWR|File::CREAT) {
    array[4] = "a new line 4"
}

# The tied array can be manipulated like a normal array,
# so there is no need for a special API, and the recno_demo program
# to demonstrate is API is useless


# tied array demo: show how to use array with a tied file
filename = "db_file.txt"
lines = Array.new
File.unlink(filename) if File.exists?(filename)
lines.tie(filename, File::RDWR | File::CREAT) {
    # first create a textfile to play with
    lines[0] = "zero"
    lines[1] = "one"
    lines[2] = "two"
    lines[3] = "three"
    lines[4] = "four"

    # print the records in order.
    # Opposed to perl, the tied array behaves exactly as a normal array
    puts "\nOriginal"
    for i in 0..(lines.length-1)
        puts "#{i}: #{lines[i]}"
    end

    #use push and pop
    a = lines.pop
    lines.push("last")
    puts("The last line was [#{a}]")

    #use shift and unshift
    a = lines.shift
    lines.unshift("first")
    puts("The first line was [#{a}]")

    # add record after record 2
    i = 2
    lines.insert(i + 1, "Newbie")

    # add record before record one
    i = 1
    lines.insert(i, "New One")

    # delete record 3
    lines.delete_at(3)

    #now print the records in reverse order
    puts "\nReverse"
    (lines.length - 1).downto(0){ |i|
        puts "#{i}: #{lines[i]}"
    }

}

Storing Complex Data in a DBM File

# example to store complex data in a database
# uses marshall from the standard library
require "sdbm"
db = SDBM.open("pleac14-8-database", 0666)

# convert the Objects into strings and back by using the Marshal module.
# Most normal objects can be converted out of the box,
# but not special things like procedure objects,
# IO instance variables, singleton objects

db["Tom Christiansen"] = Marshal.dump(["book author",  "tchrist@perl.com"])
db["Tom Boutell"] = Marshal.dump(["shareware author",
"boutell@boutell.com"])

name1 = "Tom Christiansen"
name2 = "Tom Boutell"

tom1 = Marshal.load(db[name1])
tom2 = Marshal.load(db[name2])

puts "Two Toming: #{tom1} #{tom2}"

if tom1[0] == tom2[0] && tom1[1] == tom2[1]
   puts "You're having runtime fun with one Tom made two."
else
   puts "No two Toms are ever alike"
end

# To change parts of an entry, get the whole entry, change the parts,
# and save the whole entry back
entry = Marshal.load(db["Tom Boutell"])
entry[0] = "Poet Programmer"
db["Tom Boutell"] = Marshal.dump(entry)
db.close

Persistent Data

# example to make data persistent
# uses Marshal from the standard lib
# Stores the data in a simple file,
# see 14.8 on how to store it in a dbm file

# The BEGIN block is executed before the rest of the script
# we use global variables here because local variables
# will go out of scope and are not accessible from the main script

BEGIN {
   $persistent_store = "persitence.dat"
   begin
     File.open($persistent_store) do |f|
       $stringvariable1 = Marshal.load(f)
       $arrayvariable2 = Marshal.load(f)
     end
   rescue
     puts "Can not open #{$persistent_store}"
     # Initialisation if this script runs the first time
     $stringvariable1 = ""
     $arrayvariable2 = []
   end
}

END {
   File.open($persistent_store, "w+") do |f|
     Marshal.dump($stringvariable1, f)
     Marshal.dump($arrayvariable2, f)
   end
}

# simple test program
puts $stringvariable1
puts $arrayvariable2
$stringvariable1 = "Hello World"
$arrayvariable2.push(5)
puts $stringvariable1
puts $arrayvariable2

Executing an SQL Command Using DBI and DBD

#!/usr/bin/ruby -w
# Ruby has a dbi module with an architecture similar
# to the Perl dbi module: the dbi module provides an unified
# interface and uses specialized drivers for each dbms vendor
#
begin
    DBI.connect("DBI:driver:driverspecific", "username", "auth") {
        |dbh|

        dbh.do(SQL1)

        dbh.prepare(SQL2){ |sth|
            sth.execute
            sth.fetch {|row|
                # ...
            }
        } # end of block finishes the statement handle
    } # end of block closes the database connection
rescue DBI::DatabaseError => e
    puts "dbi error occurred"
    puts "Error code: #{e.err}"
    puts "Error message: #{e.errstr}"
end

#!/usr/bin/ruby -w
# dbusers - example for mysql which creates a table,
# fills it with values, retrieves the values back,
# and finally destroys the table.

require "dbi"

# replacement for the User::pwnt module
def getpwent
    result = []
    File.open("/etc/passwd") {|file|
        file.each_line {|line|
            next if line.match(/^#/)
            cols = line.split(":")
            result.push([cols[2], cols[0]])
        }
    }
    result
end

begin
    DBI.connect("DBI:Mysql:pleacdatabase", "pleac", "pleacpassword") {
        |conn|

        conn.do("CREATE TABLE users (uid INT, login CHAR(8))")

        users = getpwent

        conn.prepare("INSERT INTO users VALUES (?,?)") {|sth|
            users.each {|entry|
                sth.execute(entry[0], entry[1])
            }
        }

        conn.execute("SELECT uid, login FROM users WHERE uid < 50") {|sth|
            sth.fetch {|row|
                puts row.collect {|col|
                    if col.nil?
                        "(null)"
                    else
                        col
                    end
                }.join(", ")
            }
        }

        conn.do("DROP TABLE users")
    }
rescue DBI::DatabaseError => e
    puts "dbi error occurred"
    puts "Error code: #{e.err}"
    puts "Error message: #{e.errstr}"
end

Program: ggh - Grep Netscape Global History