Fokozatos adatbázis-felépítés

(Megvalósítás) 2011. október 02. 16:36

Mi is ez?

A fokozatos fejlesztésnél az adatbázissémát nem a projekt elején próbáljuk megtervezni, hanem menet közben építjük fel, hogy tükrözze a megrendelők által támasztott követelmények változásait.

Akár tetszik, akár nem, a követelmények egy projekt előrehaladása közben mindig változnak.

Ez nem azt jelenti, hogy nem tervezünk!

A fontosabb kérdéseket előzetesen végig kell gondolnunk, tehát szükség lesz némi előzetes modellezésre, csupán a részleteket nem kell kidolgoznunk.


Elvárások, irányelvek

Ha automatizálni akarjuk az adatbázis építést, akkor az alábbi szempontok lehetnek fontosak:

  • kissebb változásokat könnyebb alkalmazni
  • az egyes újratervezések azonosíthatóak legyenek
  • egyszerű legyen felállítani egy adatbázis környezetet
  • hatékony telepítés egyik homokozóból a másikba
  • ne ismételjük az SQL kódot, egy helyen legyen
  • revizió kontroll alá helyezhetőség
  • szkriptezhetőség
  • könnyű használhatóság
  • sémákat kötegelten lehessen alkalmazni
  • szerver független
  • biztonságos legyen a használata
  • automatizálhatóság

A fenti igények és elvek figyelembevételével az alábbi megvalósítás lehet egy megoldás a problémára:


Könyvtár szerkezet

+ database_configuration/
    + database/
        + dump/
        + log/
        + schema/
    + scripts/

database/dump

Itt találhatóak az adatbázisokról készített dump-ok.
Névük: <adatbázisnév>.dump
Minden adatbázisról kettő található. Az utólsó frissitéskor készült és az az előtti (<adatbázisnév>.old.dump)

database/log

Itt találhatóak az frissítések alkalmával keletkezett log-ok.
Nevük: <mysql date>.log (pl.: 2011-01-02.log)
Minden egyszerre, egy napon, frissített adatbázis logja egy fájlba kerül.
A logokat a script/log.sh script írja ki a képeernyőre és a fájlba.

A log egy sora:

2011-09-18 11:08:36+02:00 [OK] [<username>] Start script.

database/schema

Itt találhatóak az adatbázis sémák. Minden séma egy külön könyvtárban. A könyvtár neve a séma neve is egyben.
A könyvtárakban találhatóak a sémákat leíró fájlok.
Ezek a fájlok az alábbi típusuak lehetnek:
*.sql, *.php, *.py, *.sh

database/script

Itt találhatóak a végrehajtó scriptek.


Szkriptek

script/log.sh

A logolást segítő függvények találhatóak benne.

#!/bin/sh
#
LOG_PATH=`readlink -f "$SCRIPT_PATH/../database/log"`
LOG_FILENAME=$LOG_PATH/`date +"%Y-%m-%d"`.log

LEVEL_ERR="\033[1;31m"
LEVEL_WARN="\033[1;33m"
LEVEL_INFO="\033[36m"
LEVEL_OK="\033[0;0m"
LEVEL_SUCCESS="\033[1;32m"
LEVEL_LOG="\033[0;0m"
LAST_LOG_MESSAGE=""

log (){
    echo -n "[    ] $@"
    LAST_LOG_MESSAGE="$@"
}

dump (){
    log $1
    loge $2
}

log_to_file (){
    echo "`date +"%F %T%:z"` [$1] [`whoami`] $2" >> $LOG_FILENAME
}

loge (){
    if [ $1 -eq 0 ];then
        echo "\r[${LEVEL_OK} OK ${LEVEL_LOG}]"
        log_to_file "OK" "$LAST_LOG_MESSAGE"
    elif [ $1 -eq 100 ];then
        echo "\r[${LEVEL_ERR}FAIL${LEVEL_LOG}]"
        log_to_file "ERR" "$LAST_LOG_MESSAGE"
    elif [ $1 -eq 200 ];then
        echo "\r[${LEVEL_SUCCESS} OK ${LEVEL_LOG}]"
        log_to_file "OK" "$LAST_LOG_MESSAGE"
    elif [ $1 -eq 250 ];then
        echo "\r[${LEVEL_INFO}INFO${LEVEL_LOG}]"
        log_to_file "INFO" "$LAST_LOG_MESSAGE"
    elif [ $1 -eq 255 ];then
        echo "\r[${LEVEL_WARN}WARN${LEVEL_LOG}]"
        log_to_file "WARN" "$LAST_LOG_MESSAGE"
    else
       echo "\r[${LEVEL_ERR}ERR ${LEVEL_LOG}]"
       log_to_file "ERR" "$LAST_LOG_MESSAGE"
       exit $1
    fi
}

script/read_me

A mysql.sh helpje. mysql.sh --help parancs futtatására kerül kiírásra.

USE
mysql.sh [option0 value0] [option1 value1] ... [optionN valueN]

FOR EXAMPLE
mysql.sh -D test_db -u username -p password -uv 456 -s mySchemaName

OPTIONS
-s      Schema name to be used. [REQUIRED]
-h      Connect to the MySQL server on the given host. [default=localhost]
-u      The MySQL user name to use when connecting to the server.[REQUIRED]
-p      The password to use when connecting to the server.[REQUIRED]
-D      The MySQL database to use. [REQUIRED]
-P      The TCP/IP port number to use for the connection. [default=3306]
-uv     The update version. [REQUIRED]
--help	

script/mysql.sh

Ez az a szkript ami a schema-k alkalmazását végzi.

Az alapállapot az, hogy van telepítve a gépen egy mysql szerver. Nem kell több! Hiszen pont az a lényeg, hogy egyszerűen, egy szkript futtatásával felépítsük az adatbázisunkat.

Ahhoz, hogy meg tudjuk állapítai egy adatbázisról, hogy milyen verziójú, azt nyilván kell tartani. Legkézenfekvőbb, ha ezt magában az adatbázisban tesszük. Ez annyiból fog állni, hogy létrehozunk egy táblát, aminek lesz egy mezője és egy rekokrdja, ami az adatbázis séma verzióját fogja tárolni. Tehát egy teljesen üres adatbázis csak ezt az egy táblát fogja tartalmazni, és mivel ez egy üres adatvázis a verziója nulla lesz. Tehát minden sémának lesz egy 0. verziója. A 0.sql:

CREATE TABLE  `database_configuration` (
`version` INT NOT NULL , PRIMARY KEY ( `version` )
) ENGINE = MYISAM ;

INSERT INTO  `database_configuration` (
`version`
)
VALUES (
'0'
);

Így teljes egészében a skripre bízhatjuk az adatbázis felépítését. Az adatbázis létrehozásától az aktualis sémák alkalmazásáig.


A szkript használata és müködése

A skriptet bárhonnan elindíthatjuk.

mysql.sh -D test_db -u username -p password -uv 5 -s test1

A szkript elején a változók definiálása és a log.sh includolása történik.

#!/bin/sh
#
DB_HOST="localhost"
DB_USER=""
DB_PWD=""
DB_NAME=""
DB_PORT="3306"
DB_DEFAULT_CHARACTER_SET="utf8 COLLATE utf8_general_ci"

MYSQL_CMD="mysql"

SCRIPT_FILE=`readlink -f $0`
SCRIPT_PATH=`dirname $SCRIPT_FILE`

SCHEMA_PATH=`readlink -f "$SCRIPT_PATH/../database/schema"`
SCHEMA_SUFFIX="sql"
SCHEMA_SCRIPT_SUFFIX="sh php py"
SCHEMA_NAME=""

DUMP_PATH=`readlink -f "$SCRIPT_PATH/../database/dump"`
DUMP_FILE="$DUMP_PATH/dbdump.dump"

current_schema_version=0
update_schema_version=0

. "$SCRIPT_PATH/log.sh"

Ha a --help kapcsolóval indítjuk, akkor kiírja a read.me fájl tartalmát és kilép.

mysql.sh --help
if [ "$1" = "--help" ]
then
    cat "$SCRIPT_PATH/read.me"
    exit 0
fi

Ha nem, akkor a megadott paraméterek alapján beállítja a változókat.

dump "Start script." 0

for i in $*
do
    case $i in
        -*) p=$i ;;
        *)
        case $p in
            "-s") SCHEMA_NAME=$i;; 
            "-h") DB_HOST=$i;; 
            "-u") DB_USER=$i;;
            "-p") DB_PWD=$i;;
            "-D") DB_NAME=$i;;
            "-P") DB_PORT=$i;;
            "-uv") update_schema_version=$i;;
            *) dump "Invalid option: $p" 255;;
        esac  
        p="";; 
    esac 
done

Ha nem kerül megadásra adatbázis és séma név, akkor hibával kilép. Ha a szkript futása közben bérmilyen hiba keletkezik, jelzi azt és kilép.

if [ -z "$DB_NAME" ]; then
    dump "Missing database name." 1
fi

if [ -z "$SCHEMA_NAME" ]; then
    dump "Missing schema name." 1
fi

Majd ezek után beállítja a MYSQL_CMD értékét.

MYSQL_CMD="mysql -h $DB_HOST -P $DB_PORT -u $DB_USER -p$DB_PWD"

Majd ellenőrzi az adatbázist

check_database $DB_NAME

A mysql a information_schema.schemata táblában tárolja az adatbézisok egyes adatait, mint pl. a nevűket. Tehát, ha itt szerepel a létrehozandó adatbázisunk neve, akkor már létezik, ha nem, akkor nem létezik, és létre kell hozni.

check_database (){
    log "Check database ($1)."
    sql="SELECT schema_name FROM schemata WHERE schema_name='$1'"
    db=`$MYSQL_CMD -D information_schema -N -e "$sql"`

    if [ -z "$db" ]; then
        loge 255
        create_database $DB_NAME
        use_schema 0
    else
        loge 0
    fi
}

Ha nem létezik, akkor létrehozzuk az adatbázist, és alkalmazzuka a 0. sémát.

create_database (){
    log "Create database ($1)."
    sql="CREATE DATABASE $1 DEFAULT CHARACTER SET $DB_DEFAULT_CHARACTER_SET;"
    $MYSQL_CMD -e "$sql" > /dev/null 2>&1
    loge $?
}

Majd lekérdezzük az adatbázisunk verzióját.

get_schema_version

És értékül adjuk az current_schema_version változónak.

get_schema_version (){
    log "Get current schema version."
    sql="SELECT * FROM database_configuration;"
    current_schema_version=`$MYSQL_CMD -D $DB_NAME -N -e "$sql"` > /dev/null 2>&1
    loge $?
}

Ezek után ellenőrizzük, hogy az adatbázisunk verziója kissebb-e mint az kivánt verzió. S ha igen, akkor elkezdődik az update folyamat.

Elsőször is készítünk az adatbázisról egy dump-ot. Így bármi történik a frissítés alatt, vissza tudjuk állítani a korábbi állpotot.

make_dump
make_dump (){
    log "Make database dump($DB_NAME.dump)."
    dump_filename=$DUMP_PATH/$DB_NAME.dump
    
    if [ -f $dump_filename ]; then
        cp -fr $dump_filename "$DUMP_PATH/$DB_NAME.old.dump"
    fi

    mysqldump -h $DB_HOST -u $DB_USER -p$DB_PWD -B $DB_NAME > $dump_filename
    loge $?
}

Ha kész, akkor egyenkét alkalmazzuk a sémékt.

update_schema (){
    for i in `seq $1 $2`;do
        use_schema $i
        set_schema_version $i
    done
}

Minden séma fájl egyenkét kerül alakalmazásra.

use_schema (){
    log "The $1. schema updating."
    filename="$SCHEMA_PATH/$SCHEMA_NAME/$1"
    cmd="$MYSQL_CMD -D $DB_NAME"
    tmp=`mktemp`
    res=1
    
    if [ -f "$filename.$SCHEMA_SUFFIX" ]; then
        res=0
        $cmd < "$filename.$SCHEMA_SUFFIX" > $tmp 2>&1

        if [ $? -ne 0 ]; then
            loge 100
            err=`cat $tmp`
            rm -f $tmp
            dump "$err" 1
        fi
    else
        for suffix in $SCHEMA_SCRIPT_SUFFIX;do
            if [ -f "$filename.$suffix" ]; then
                res=0
                loge 250
                log "'$1.$suffix' schema script executing."
                chmod +x "$filename.$suffix"
                "$filename.$suffix" $DB_NAME $DB_HOST $DB_PORT $DB_USER $DB_PWD > $tmp 2>&1
                if [ $? -ne 0 ]; then
                    loge 100
                    err=`cat $tmp`
                    rm -f $tmp
                    dump "$err" 1
                fi
            fi
        done    
    fi
    rm -f $tmp
    loge $res
}

Alapesetben minden séma egy SQL utasítás és egy *.sql nevű fájlban található. Ilyenkor csak egyszerüen alkalmazásra kerül a séma.

Előfordulhatnak olyan helyzetek is, amikor egy egyszerű SQL utasítás nem elég, mert nem lehet vele végrehajtani a változtatásokat. Ilyenkor scriptet kell írni.

A szkript három féle típust támogat (sh php py). Ha a séma ezek egyike, akkor azt futtatja, és az alábbi változók értékét ebben a sorrrendben kapja meg: $DB_NAME $DB_HOST $DB_PORT $DB_USER $DB_PWD. Ha a szkript nem tér vissza hibával, akkor tovább alkalmazza a sémákat.

Ha egy séma sikeresen alkalmazásra került, akkor beállítja az adatbázis új verzióját:

set_schema_version (){
    log "Schema version updating ($i)."
    sql="UPDATE database_configuration SET version = '$1';"
    $MYSQL_CMD -D $DB_NAME -e "$sql" 1>/dev/null
    loge $?
}

Ha minden séma alkalmazása sikeresen befejeződött, akkor Schema uptating successful. üzenettel kilép.

Ez a megvalósítás letölthető: database_configuration.tar.gz

Felhasznált irodalom:

  • Scott W. Ambler, Pramod J. Sadalage: Refactoring - Adatbázisok újratervezése Fokozatos adatbázis-felépítés
  • Martin Gruber: SQL A-Z
  • Büki András: UNIX/Linux héjprogramozás
Vissza