images/kirby1.jpg

Figure: Kirby en posture d'attaque... ou presque

Table des matières

  1. Introduction

  2. Se connecter à une base de données

  3. Créer une table

    1. Types de champs

    2. Le champ recno

    3. Spécifier des valeurs par défaut

    4. Spécifier des champs obligatoires

    5. Les index

    6. Note sur les index

    7. Champs associés

    8. Liens un-à-n

    9. Champs calculés

  4. Utiliser une table existante

  5. La méthode insert

  6. Sélectionner des articles

    1. La méthode select

    2. Sélectionner avec des index

    3. Sélectionner avec l'index :recno

    4. Sélectionner avec des tables associées ou des liens un-à-n

  7. Le KBResultSet (ensemble résultat)

    1. Trier un ensemble résultat

    2. Produire un état avec un ensemble résultat

    3. Tableaux croisés, tables à pivots, tableaux de colonnes (je ne sais pas comment les appeler !)

  8. Mettre à jour des articles

    1. La méthode update

    2. La méthode set

    3. La méthode update_all

  9. Supprimer des articles

    1. La méthode delete

    2. La méthode clear (ou delete_all)

  10. La méthode pack

  11. Champs memo et blob

  12. Méthodes diverses de KirbyBase

    1. KirbyBase#drop_table

    2. KirbyBase#tables

    3. KirbyBase#table_exists?

    4. KirbyBase#rename_table

  13. Miscellaneous KBTable methods

    1. KBTable#[]=

    2. KBTable#[]

    3. KBTable#field_names

    4. KBTable#field_types

    5. KBTable#total_recs

    6. KBTable#import_csv

    7. KBTable#add_column

    8. KBTable#drop_column

    9. KBTable#rename_column

    10. KBTable#change_column_type

    11. KBTable#change_column_defaul_value

    12. KBTable#change_column_required

    13. KBTable#add_index

    14. KBTable#drop_index

  14. Les caractères spéciaux dans les données

  15. Structure des tables

  16. Notes sur le serveur

  17. Conseils pour améliorer les performances

  18. Diagramme mémoire du mode Client/Serveur

  19. Diagramme mémoire du mode mono-utilisateur

  20. Licence

  21. Crédits

  22. Les champs de type YAML
  23. Un article sur KirbyBase

Introduction

KirbyBase est un système de gestion de bases de données écrit en ruby, simple, basé sur des fichiers texte. Ses caractéristiques principales sont :

KitbyBase satisfait de nombreux besoins de gestion de bases de données, et se situe quelque part entre la gestion de fichiers plats et de petits SGBD relationnels tels que SQLite et MySQL.

Envoyez moi un mot !

Si vous trouvez un usage à KirbyBase, soyez assez gentil pour m'envoyer un email m'indiquant ce que vous en faites. Entendre parler de ce que les gens font de KirbyBase est une des plus grandes récompenses que je reçois pour avoir travaillé à le bâtir.

Se connecter à une base

Pour utiliser Kirbybase, vous devez d'abord requérir le module :

require 'kirbybase'

Puis créer une instance :

db = KirbyBase.new

Par défaut l'instance est une connection locale utilisant le même espace mémoire que votre programme appelant. Pour spécifier un lancement en mode client/serveur il faut créer l'instance comme ceci :

db = KirbyBase.new(:client, 'localhost', 44444)

Vous devez mettre l'adresse IP de votre serveur à la place de localhost et du numéro de port.

Pour indiquer un emplacement de la base autre que le répertoire du programme appelant, ajoutez le nom du répertoire en argument, comme suit :

db = KirbyBase.new(:local, nil, nil, './data')

KirbyBase considère comme table tout fichier du répertoire indiqué qui a la bonne extension. L'extension par défaut est ".tbl". Vous pouvez spécifier une extension différente en la passant en argument :

db = KirbyBase.new(:local, nil, nil, './', '.dat')

Vous pouvez aussi fournir ces arguments via un bloc de code. Donc, si vous ne voulez pas avoir à spécifier un tas d'arguments juste pour préciser celui qui vous intéresse, mettez le dans un bloc attaché à l'appel de la méthode. Ce qui précède peut donc s'écrire comme ceci :

db = KirbyBase.new { |d| d.ext = '.dat' }

Créer une table

Pour créer une table, vous spécifiez son nom, puis un nom et un type pour chaque champ. Par exemple pour créer une table sur les avions de la seconde guerre mondiale :

plane_tbl = db.create_table(:plane, :name, :String, :country, :String,
:role, :String, :speed, :Integer, :range, :Integer, :began_service, :Date,
:still_flying, :Boolean)
ndt: le fichier de la table s'appellera plane.tbl et non plane_tbl.tbl

Les types de champs de KirbyBase

Tip

:String, :Integer, :Float, :Boolean(true/false), :Time, :Date, :DateTime, :Memo, :Blob, et :YAML.

Le champ recno

Important

KirbyBase crée automatiquement une clé primaire, que j'appelle recno, de type :Integer, pour chaque table. Ce champ est incrémenté automatiquement à chaque fois qu'un nouvel enregistrement est inséré. Vous pouvez utiliser ce champ dans les requêtes de sélection, mise à jour, et suppression, mais vous ne pouvez pas le modifier.

Vous pouvez aussi spécifier que la table doit être cryptée. Elle sera écrite sur disque sous forme cryptée (ou plutôt brouillée) avec un code de type Vignere. Ce code est semblable à un rot13 (rotation binaire), mais avec une clé pour déterminer les substitutions de caractères à effectuer. Attention, ce cryptage ne fera que ralentir le plus amateur des hackers. Ne comptez pas sur lui pour protéger des données sensibles ! Vous spécifiez le cryptage en utilisant un bloc de code associée à #create_table :

plane_tbl = db.create_table(:plane, :name, :String...) do |t|

    t.encrypt = true
end

Vous pouvez aussi spécifier que vous voulez que les articles de la table vous soient restitués sous forme d'instances de classe. Pour cela définissez une classe avant d'appeler #create_table. Cette classe doit avoir un accesseur pour chaque nom de champ de la table. Elle doit aussi avoir une méthode de classe appelée #kb_create, qui retourne une nouvelle instance de classe. Voici un exemple :

class Foobar

    attr_accessor(:recno, :name, :country, :role, :speed, :range,
:began_service, :still_flying, :alpha, :beta)

def Foobar.kb_create(recno, name, country, role, speed, range,
began_service, still_flying)
name ||= 'No Name!'
speed ||= 0
began_service ||= Date.today

Foobar.new do |x|
x.recno = recno
x.name = name
x.country = country
x.role = role
x.speed = speed
x.range = range
x.began_service = began_service
x.still_flying = still_flying
end
end

def initialize(&block)
instance_eval(&block)
end
end

Passez cette classe à #create_table dans un bloc de code comme dans :

plane_tbl = db.create_table(:plane, :name, :String...) do |t|
t.record_class = Foobar
end

Maintenant, quand vous appelez #select, le résultat sera sous la forme d'instances de Foobar, au lieu d'être comme par défaut des instances de Struct. Ceci marche aussi dans l'autre sens. Vous pouvez fournir des instances de Foobar en entrée des méthodes #insert, #update et #set. On reparle de ces méthodes plus loin.

Important

La méthode #create_table retourne une référence à une instance de KBTable. C'est le seul moyen, à part appeler KirbyBase#get_table, d'obtenir une « poignée » (handle) vers une instance de KBTable. Vous ne pouvez pas appeler KBTable#new directement.

Spécifier des types de champs avancés

Il y a quatre types de champs avancés : valeurs par défaut (Defaults), champs requis (Required), les index et les « extras » (Lookup – associations, Link_many – liens un-à-n, Calculated – champs calculés).

Valeurs par défaut

Normalement, quand vous insérez un article, si vous ne fournissez pas une valeur pour un champ, ou que vous indiquez nil comme valeur, KirbyBase stocke cela sous forme d'une chaîne vide (i.e. "") dans le fichier de la table, et rend un nil quand vous le sélectionnez.

Cependant, vous pouvez indiquer à KirbyBase que vous voulez que la colonne ait une valeur par défaut. Cette valeur sera utilisée à chaque fois qu'aucune valeur ou qu'une valeur nil sera indiquée pour le champ en insertion. En mise à jour, les valeurs par défaut ne seront pas utilisées.

Par exemple, pour que la colonne category aie une valeur par défaut, vous écrirez :

db.create_table(:addressbook, :firstname, :String,
:lastname, :String, :phone_no, :String,
:category, {:DataType=>:String, :Default=>'Business'})

Remarquez que, du fait que nous spécifions un type de champ avancé, nous ne pouvons pas simplement mettre :String dans la deuxième moitié de la définition du champ. Nous devons passer un hash, avec un item :DataType à la valeur :String. Ensuite, puisque nous voulons préciser une valeur par défaut, nous devons inclure un item de hash nommé :Default avec la valeur que nous voulons pour défaut. La valeur par défaut doit être d'un type valide pour ce champ.

Champs obligatoires (required)

Normalement, quand vous insérez ou mettez à jour un article, si vous ne fournissez pas une valeur pour un champ, ou que vous indiquez nil comme valeur, KirbyBase stocke cela sous forme d'une chaîne vide (i.e. "") dans le fichier de la table, et rend un nil quand vous le sélectionnez.

Cependant vous pouvez indiquer à KirbyBase que vous voulez qu'une colonne soit obligatoirement valorisée (càd que vous devez lui donner une valeur et que cette valeur ne peut être nil). Quand un article est inséré ou mis à jour, une exception sera levée pour chaque champ obligatoire pour lequel aucune valeur ou une valeur nil aura été donnée.

Par exemple, pour indiquer que la colonne category est obligatoire, vous écrirez :

db.create_table(:addressbook, :firstname, :String,
:lastname, :String, :phone_no, :String,
:category, {:DataType=>:String, :Required=>true})

Remarquez que, du fait que nous spécifions un type de champ avancé, nous ne pouvons pas simplement mettre :String dans la deuxième moitié de la définition du champ. Nous devons passer un hash, avec un item :DataType à la valeur :String. Ensuite, puisque nous voulons spécifier que le champ est obligatoire, nous devons inclure un item de hash nommé :Required avec true pour valeur.

Les index

Les index sont des tableaux en mémoire qui contiennent une entrée pour chaque article d'une table, contenant la valeur du champ spécifié lors de la création de l'index, plus le :recno de l'article dans la table. Les tableaux d'index étant plus petits que les tables, et se trouvant en mémoire et non sur disque, leur usage dans les requêtes est habituellement beaucoup plus rapide que la recherche dans les tables elles-mêmes, surtout quand la table est grande.

Pour spécifier qu'un index doit être créé, vous devez dire à KirbyBase quels champs doivent être inclus dans un index particulier. Vous pouvez avoir jusqu'à 5 index par table. Les index peuvent être mono ou multi-champs. Par exemple pour créer un index sur nom et prénom dans un table nommée :agenda, vous coderez :

db.create_table(:agenda, 
:nom, {:DataType=>:String, :Index=>1},
:prenom, {:DataType=>:String, :Index=>1},
:telephone, :String)

Notez que, du fait que vous utilisez des types de champs avancés, vous ne pouvez pas simplement écrire :String dans la seconde partie de la définition. Vous devez en fait passer un hash (table de hachage), avec le type d'item valant :String. De plus, parce que vous créez un index, vous devez fournir un autre élément de hash appelé :Index et valant quelque chose entre 1 et 5. Pour les index composés (multi-champs), comme celui de l'exemple, tous les champs composant l'index doivent avoir le même numéro d'index (1 dans l'exemple). Pour avoir un autre index sur la table, n'oubliez pas d'incrémenter le numéro d'index. Par exemple, si on veut avoir aussi un index sur le numéro de téléphone dans notre agenda, on codera :

db.create_table(:agenda, :nom, {:DataType=>:String, :Index=>1},
:prenom, {:DataType=>:String, :Index=>1},
:telephone, {:DataType=>:String, :Index=>2})

Remarquez comment on a incrémenté à 2 le numéro d'index sur le champ telephone. Du fait qu'il n'y a pas d'autre champ ayant le même numéro d'index, ceci créera un index avec le seul numéro de téléphone. Vous verrez plus loin comment on utilise les index dans les requêtes de sélection.

Note sur les index

Quand KirbyBase démarre, il crée une instance pour chaque chaque table de la base de données. Il crée aussi un tableau d'index pour chaque champ indexé de chaque table. S'il y a beaucoup de grandes tables avec des champs indexés, cette étape d'initialisation peut prendre du temps. J'ai décidé de faire comme ça, car j'ai pensé qu'il vaut mieux attendre au démarrage de l'application qu'au moment du premier accès à une table. Un utilisateur est habitué à attendre qu'une application démarre, si bien que cette attente initiale lui paraîtra plus naturelle qu'en cours d'exécution.

Faites moi savoir si cette option vous pose un problème dans vos applications. J'ai des idées sur la façon dont je pourrais implémenter un choix du moment où le développeur décide de créer les index.

Ceci est une excellente raison d'utiliser les capacités client/serveur de KirbyBase. En mode client/serveur, l'initialisation de la base se fait sur le serveur, si bien que, une fois que c'est fait, les clients qui démarrent leurs applications et se connectent au serveur n'auront aucune attente liée à la création des tableaux d'index.

Champs associées - Lookup Fields

Les champs associés sont des champs qui contiennent une référence à un article d'un autre table. Par exemple, disons que vous avez une table nommée :service qui a pour champs :dept_id, :dept_name, and :manager. Maintenant disons que vous ne voulez pas que le champ :manager contienne seulement le nom du chef ; vous voulez qu'il pointe vers l'article relatif au chef dans la table des employés :employee – la table associée, qui a des champs du genre :employee_id, :firstname, :lastname, :ss_no, etc. Voici comment on fait ça :

db.create_table(:service, :dept_id, :String, :dept_name, :String,
:manager, {:DataType=>:String, :Lookup=>[:employee, :employee_id]})

Ce qu'on a fait ici est de dire un petit quelque de plus (un « extra ») au sujet du champ :manager. Nous indiquons qu'à chaque fois qu'on cherche la valeur de :manager, nous voulons que KirbyBase fasse aussi un #select sur la table :employee en y cherchant les valeurs associées à l'article ayant pour valeur de champ :service.manager la même valeur que le champ :employee.employee_id. Si un index est disponible pour le champ :employee.employee_id, KirbyBase l'utilisera automatiquement.

Il y a un raccourci pour spécifier un champ associé. Si le champ :employee_id a été désigné comme champ clé pour la table :employee, nous pouvons encore raccourcir notre code et KirbyBase s'en débrouillera. Par exemple, si la table :employee a été crée comme suit :

db.create_table(:employee, :employee_id, {:DataType=>:String, :Key=>true},
:firstname, :String, :lastname, :String)

Alors la définition des champs de :manager pourra être récrite comme :

db.create_table(:service, :dept_id, :String, :dept_name, :String,
:manager, :employee)

KirbyBase comprendra alors que vous voulez associer :service.manager à :employee.employee_id.

Important

Pour pouvoir utiliser le raccourci pour définir un champ associé, la table associée doit déjà exister. Alors KirbyBase peut exercer sa « magie » d'association via le nom du champ.

Liens un-à-n

Quand vous spécifiez qu'un champ a un lien un-à-n (Link_many), vous indiquez à KirbyBase que vous voulez créer une relation un-à-n entre ce champ et un ensemble d'articles d'une autre table.

Par exemple, disons que vous créez un logiciel de prise de commandes pour votre job. Vous avez une table maîtresse nommée :orders (commandes) qui contient un article pour chaque commande passée par un client. Elle a des champs du genre : :order_id, :cust_id, :order_date, etc.

Maintenant vous avez aussi besoin d'une table qui aura un article pour chaque item commandé. Appelons-la :order_items et ses champs pourraient être : :item_no, :qty, :order_id.

Notez que la table détail a un champ nommé :order_id. Il contiendra le lien avec la table des commandes et son champ :orders.order_id. Si un client commande un seul type d'items, il n'y aura qu'un seul article dans la table :order_items qui aura cet order_id. Mais, si le client passe commandes de plusieurs types d'items, il y aura autant d'articles dans la table :order_items avec ce même order_id. C'est pourquoi on appelle ça une « relation un-à-n » entre la table maîtresse (ou parente), :orders, et la table détail (ou enfant), :order_items.

Quand nous sélectionnons un article dans :order, nous voulons disposer aussi des articles correspondants de la table :order_items. Nous obtenons cela en indiquant à KirbyBase la relation entre les deux. Voici comment :

db.create_table(:orders, :order_id, :Integer, :cust_id, :Integer,
:order_date, :Date, :items, {:DataType=>:ResultSet, :Link_many=>[:order_id,
:order_items, :order_id]})

db.create_table(:order_items, :item_no, :Integer, :qty, :Integer,
:order_id, :Integer)

Regardez la clause :Link_many dans le hash de définition de l'exemple ci-dessus. Nous spécifions qu'un champ nommé :items, sera créé avec le type :ResultSet. Nous disons à KirbyBase que, quand nous demanderons la valeur de :items, il devra aussi faire un #select sur :order_items et rendre un KBResultSet (ensemble de résultat de KirbyBase) qui contiendra les :order_items dont le champ :order_id (le dernier du tableau associé au hash), a la même valeur que le champ :orders.order_id field (le premier du même tableau).

Si vous ouvrez la table :orders avec un éditeur de textes, vous remarquerez que, pour chaque article, le champ :items est à blanc. Aucune donnée n'est jamais stockée dans ce champ, puisqu'il est toujours calculé pendant l'exécution du programme.

Champs calculés

Quand vous spécifiez qu'un champ est un champ calculé (Calculated field), vous dites à KirbyBase de calculer la valeur de ce champ pendant l'exécution du programme, sur la base de l'expression que vous avez indiquée au moment de la création de la table.

Par exemple, vous avez une table pour suivre vos achats. Elle a des champs comme :purchase_date, :description, :qty, and :price.

Disons que vous voulez avoir un champ "virtuel", nommé :total_cost, qui est le produit de la quantité par le prix unitaire. Vous voulez calculer ce champ à l'exécution, de façon à pouvoir changer la quantité ou le prix et que cela se reflète automatiquement dans ce total. Voici la définition de la table:

db.create_table(:purchases, :purchase_date, :Date, :description, :String,
:qty, :Integer, :price, :Float, :total_cost, {:DataType=>:Float,
:Calculated=>'qty*price'})

La valeur du champ calculé est donc définie par l'expression 'qty*price'.

Si vous ouvrez le fichier de la table :purchases avec un éditeur, vous verrez que le champ :total_cost est toujours à blanc. Aucune valeur n'est jamais stockée dans ce champ, car sa valeur est calculée à l'exécution.

Accéder à une table existante

Si une table existe déjà et que vous voulez y accéder, vous devez utiliser la méthode #get_table sur votre instance de base. Vous ne pouvez appeler KBTable#new directement.

plane_tbl_another_reference = db.get_table(:plane)

Alors vous pouvez utiliser la table comme vous l'auriez fait après l'avoir créée.

La méthode insert

Pour insérer des articles dans un table, vous utilisez la méthode insert. Pour fournir les données à insérer, vous pouvez utiliser un tableau, un hash, une struct, un bloc de code, ou une instance de l'article de la table.

Insérer un article via un tableau :

plane_tbl.insert('FW-190', 'Germany', 'Fighter', 399, 499,
Date.new(1942,12,1), false)

La longueur du tableau doit être égale au nombre de colonnes de la table, sans compter la colonne :recno. De plus, les types de données doivent concorder. Dans l'exemple ci-dessus, indiquer "399" au lieu de 399 aurait provoqué une erreur.

Insérer un article via un hash :

plane_tbl.insert(:name='P-51', :country='USA', :role='Fighter', :speed=403,
:range=1201, :began_service=Date.new(1943,6,24), :still_flying=true)

Insérer un article via une Struct :

InputRec = Struct.new(:name, :country, :role, :speed, :range,
:began_service, :still_flying)
rec = InputRec.new('P-47', 'USA', 'Fighter', 365, 888, Date.new(1943,3,12),
false)

plane_tbl.insert(rec)

Insérer un article via un bloc de code:

plane_tbl.insert do |r|
r.name = 'B-17'
r.country = 'USA'
r.role = 'Bomber'
r.speed = 315
r.range = 1400
r.began_service = Date.new(1937, 5, 1)
r.still_flying = true
end

Insérer un article via une instance de la classe associée :

foo = Foobar.new do |x|
x.name = 'Spitfire'
x.country = 'Great Britain'
x.role = 'Fighter'
x.speed = 333
x.range = 454
x.began_service = Date.new(1936, 1, 1)
x.still_flying = true
x.alpha = "Cette variable ne sera pas stockée dans KirbyBase."
x.beta = 'Ni celle-ci.'
end

plane_tbl.insert(foo)

Note

La méthode #insert retourne le numéro (recno) du nouvel article. Il s'agit d'un entier auto-incrémenté par KirbyBase. Ce nombre ne changera jamais pour cet article et peut être utilisé comme identifiant unique pour lui.

Sélectionner des articles

La syntaxe que vous employez pour sélectionner les articles que vous voulez manipuler est la même que ce soit pour un select, update, ou delete, aussi je vais d'abord décrire en général comment créer une expression de recherche, puis parler des spécificités de select, update, et delete.

Dans KirbyBase, les conditions de recherche sont spécifiées avec des blocs de code ruby. N'importe quel bloc de code qui peut être converti en objet Proc est valide. Cela donne une grande flexibilité, comme vous le verrez dans les nombreux exemples qui suivent.

Tip

Vous trouverez beaucoup d'exemples de requêtes dans le répertoire "examples" de la distribution de KirbyBase .

Maintenant que nous avons une compréhension générale de la façon de sélectionner des articles pour agir dessus, nous allons regarder plus précisément les méthodes select (sélectionner), update (mettre à jour), et delete (effacer).

La méthode select

La méthode select vous permet de demander tous les articles d'une table qui répondent à un certain critère de sélection. De plus vous pouvez spécifier quels champs vous voulez inclure dans l'ensemble résultat. La méthode select rend une instance de KBResultSet (ensemble résultat de KirbyBase), qui est un tableau d'articles qui satisfont au critère de sélection. KBResultSet est détaillé plus loin.

Voici l'exemple le plus simple de select :

result_set = plane_tbl.select

Aucun bloc de code n'étant spécifié, KirbyBase va inclure tous les articles de la table dans l'ensemble résultat. Aucune liste des champs à inclure dans l'ensemble résultat n'étant spécifiée, KirbyBase va prendre tous les champs de chaque article.

Pour indiquer que vous ne voulez qu'une partie des champs, vous listez leurs noms de champ en argument de la méthode select. Par exemple :

result_set = plane_tbl.select(:name, :speed)

Pour indiquer un critère de sélection, attachez un bloc de code à l'appel de la méthode. Par exemple, si vous ne voulez que les avions Japonais :

result_set = plane_tbl.select(:name, :speed) { |r| r.country == 'Japan' }

Vous pouvez combiner des expressions dans le bloc de code. Par exemple, pour sélectionner seulement les avions US ayant une vitesse > 350 mph :

result_set = plane_tbl.select { |r| r.country == 'USA' and r.speed > 350 }

Vous pouvez utiliser les expressions régulières dans le bloc de code. Sélectionnons les avions de combat de l'Axe :

result_set = plane_tbl.select do |r|
r.country =~ /Germany|Japan/ and r.role == 'Fighter'
end

Sélectionner avec un index

Une requête avec un index est presque comme un select ordinaire. Vous avez seulement à spécifier la méthode select propre à l'index à utiliser.

Par exemple, disons que vous avez créé un index sur le champ :speed de la table :plane. Vous voulez les avions ayant une vitesse supérieure à 400 mph. Ruby crée automatiquement les méthodes select pour chacun des index de la table. Donc vous pouvez écrire votre requête comme ceci :

plane_tbl.select_by_speed_index { |r| r.speed > 400 }

Remarquez que vous avez juste à construire le nom de méthode en y incluant le nom du champ index. C'est aussi simple que ça.

Pour les index composés, vous donnez les noms des champs indexés dans le même ordre que celui qu'ils ont dans la table. Si vous avez indexé la table :plane sur :country et :role, en un seul index, pour effectuer une recherche sur cet index composé, vous écrirez :

plane_tbl.select_by_country_role_index do |r|
r.country == 'Germany' and r.role == 'Fighter' }
end

Remarquez comme on a juste à énumérer les champs composant l'index, séparés par des blancs soulignés ( _ ).

Warning

Si vous spécifiez une méthode select qui n'existe pas, vous aurez une erreur. Idem si vous spécifiez une requête en incluant un nom de champ qui n'est pas dans l'index.

Sélectionner avec le :recno

Pour chaque table, un index basé sur le :recno est créé automatiquement, que vous créiez ou non d'autres index. Vous pouvez utiliser cet index pour sélectionner des articles. Par exemple:

plane_tbl.select_by_recno_index { |r| [3, 45, 152].include?(r.recno) }

Sélectionner avec des tables associées ou des liens un-à-n

Les sélections qui mettent en jeu des tables associées ou des liens un-à-n sont un cas à part, car ces deux types de champs retournent des objets complexes et non pas des types simples comme string ou integer. Par exemple, considérons le cas de table associée décrit ci-dessus. Pour mémoire, voici à nouveau la définition des deux tables :

service_tbl = db.create_table(:service, :dept_id, :String,
:dept_name, :String, :manager, {:DataType=>:String, :Lookup=>[:employee,
:employee_id]})

employee_tbl = db.create_table(:employee, :employee_id, {:DataType=>:String,
:Key=>true}, :firstname, :String, :lastname, :String)

Pour trouver le département géré par John Doe, la requête select aurait cette allure :

service_tbl.select do |r|
r.manager.firstname == 'John' and r.manager.lastname == 'Doe'
end

Pour imprimer tous les départements avec le nom de leur chef :

service_tbl.select.each do |r|
puts 'Department: %s Manager: %s %s' % [r.dept_name,
r.manager.firstname, r.manager.lastname]
end

Les selects mettant en jeu les liens un-à-n sont légèrement différents parce qu'ils impliquent des ensembles résultats (ResultSets) au lieu de simples objets. Voici la définition des tables de l'exemple utilisé précédemment pour décrire les liens un-à-n :

orders_tbl = db.create_table(:orders, :order_id, :Integer,
:cust_id, :Integer, :order_date, :Date, :items, {:DataType=>:ResultSet,
:Link_many=>[:order_id, :order_items, :order_id]})

order_items_tbl = db.create_table(:order_items, :item_no, :Integer,
:qty, :Integer, :order_id, :Integer)

Pour imprimer une commande et toutes les lignes de détail associées :

result = order_tbl.select { |r| r.order_id == 501 }.first
puts '%d %d %s' % [result.order_id, result.cust_id, result.order_date]

result.items.each { |item| puts '%d %d' % [item.item_no, item.qty] }

Les attributs des items dans l'ensemble résultat sont eux-mêmes un ensemble résultat contenant tous les articles :order_items qui relèvent de la commande sélectionnée.

L'ensemble résultat - KBResultSet

Comme indiqué ci-dessus, la méthode select rend une instance de KBResultSet, qui est un tableau d'objets de type Struct (ou des instances de la classe spécifiée dans record_class), chaque objet représentant un article qui satisfait aux critères de sélection.

Chaque item dans KBResultSet étant un objet de type Struct, vous pouvez facilement référencer ses membres en utilisant des noms de champs. Donc, pour imprimer le nom et la vitesse de chaque avion allemand de la table, vous écrirez :

plane_tbl.select(:name, :speed) { |r| r.country == 'German' }.each do |r|
puts '%s %s' % [r.name, r.speed]
end

Trier un ensemble résultat

Vous pouvez spécifier des critères de tri en appelant KBResultSet#sort. Vous devez indiquer la liste des champs sur lesquels vous voulez trier. Par exemple, pour sélectionner tous les avions, en récupérer les noms, pays et vitesse, et trier le résultat par pays (ascendant) et nom (ascendant), vous écrirez :

result = plane_tbl.select(:name, :country, :speed).sort(:country, :name)

Pour indiquer que vous voulez trier un champ par ordre descendant, vous devez mettre un signe moins (-) devant le nom du champ. Par exemple, pour sélectionner tous les avions, en récupérer les noms, pays et vitesse, et trier le résultat par pays (ascendant) et vitesse (descendant), vous écrirez :

result_set = plane_tbl.select(:name, :country, :speed).sort(:country, -:speed)


Vous pouvez aussi spécifier explicitement qu'un champ doit être trié par ordre ascendant en mettant un signe plus (+) devant son nom.

Produire un état à partir de l'ensemble résultat

Vous pouvez transformer le KBResultSet en un beau petit rapport, prêt à être imprimé, en appelant KBResultSet#to_report. Pour imprimer un état formaté de tous les avions, avec les noms, pays, vitesses, triés par nom, vous écrirez :

puts plane_tbl.select(:name, :country, :speed).sort(:name).to_report

Tableaux croisés, tables à pivot, tableaux de colonnes (je ne sais pas comment les appeler !)

Chaque KBResultSet a un attribut supplémentaire qui peut être très utile. Quand l'ensemble résultat est créé, KirbyBase crée un tableau pour chaque nom de colonne qui contient toutes les valeurs de la colonne. Un exemple rendra ça plus clair. Vous avez une table qui est comme ceci :

name

speed

range

P-51

402

1201

ME-109

354

544

Spitfire

343

501

Si vous faites un select sur cette table, non seulement l'ensemble résultat contiendra une ligne pour chaque article qui répond aux critères de la sélection, mais il contiendra aussi un tableau pour chaque colonne, qui contiendra toutes les valeurs de la colonne :

result = plane_tbl.select

puts result[0].name => P-51
puts result[0].speed => 402

p result.speed => [402,354,343]

Vous pouvez référencer ce tableau par colonne en utilisant le nom de la colonne comme une méthode. KirbyBase rend un tableau qui contient toutes les valeurs de la vitesse – dans notre exemple. Cela peut être très utile. Regardez l'exemple dans examples\crosstab_test de la distribution.

Mettre à jour des articles

Vous pouvez mettre à jour les données d'une table soit en utilisant la méthode KBTable#update seule, soit en conjonction avec la méthode KBResultSet#set . Les deux procédés donnent le même résultat. La principale différence est que, en utilisant la méthode #update seule, vous pouvez utiliser un Hash, un tableau ou une Struct comme critère de mise à jour, tandis que l'utiliser avec la méthode #set ajoute la possibilité d'utiliser un bloc de code. Des exemples suivent.

La méthode update

Pour mettre à jour une table, vous pouvez utiliser la méthode update. Vous devez spécifier un bloc de code qui indique quels articles doivent être modifiés. De plus vous devez spécifier les champs à modifier et les nouvelles valeurs pour ces champs.

Vous pouvez mettre à jour les articles en utilisant un hash, un tableau, une struct, ou une instance de la classe que vous avez définie avec la table. Par exemple pour changer la vitesse du P-51 à 405mph et sa portée à 1210 miles, vous écrirez :

plane_tbl.update(:speed=>405, :range=>1210) { |r| r.name == 'P-51' }

ou :

UpdateRec = Struct.new(:name, :country, :role, :speed, :range,
:began_service, :still_flying)

rec = UpdateRec.new
rec.speed = 405
rec.range = 1210
plane_tbl.update(rec) { |r| r.name == 'P-51' }

La méthode set

Vous pouvez aussi mettre à jour les articles avec un bloc de code, via KBResultSet#set :

plane_tbl.update {|r| r.name == 'P-51'}.set do |r|
r.speed = 405
r.range = 1210
end

De même vous pouvez utiliser un hash , une struct ou un tableau, via KBResultSet#set :

plane_tbl.update {|r| r.name == 'P-51'}.set(:speed=>405, :range=>1210)

Important

Quand vous ne fournissez pas un bloc de code à la méthode #select, KirbyBase sélectionne automatiquement tous les articles de la table. J'ai trouvé que faire de même pour la méthode #update était trop dangereux. Il serait trop facile de modifier accidentellement tous les articles de la table en oubliant de fournir un bloc de code à #update. C'est pourquoi la méthode #update requiert un bloc de code. Pour mettre à jour tous les articles de la table, utilisez la méthode #update_all .

Note

La méthode #update retourne un entier spécifiant le nombre d'articles qui ont été modifiés.

La méthode update_all

Pour mettre à jour tous les articles d'une table, vous pouvez utiliser la méthode KBTable#update_all. Elle marche comme la méthode update, sauf qu'il n'y a pas à spécifier de bloc de code avec des critères de sélection.

Par exemple, pour ajouter 50 mph à chaque vitesse, vous écrirez :

plane_tbl.update_all { |r| r.speed = r.speed + 50 }

Note

La méthode #update_all rend un entier indiquant combien de lignes ont été modifiées.

Supprimer des articles

Supprimer des articles dans une table se fait comme un #select ou un #update.

La méthode delete

Pour utiliser la méthode #delete, vous devez fournir un bloc de code identifiant quels articles doivent être supprimés.

Par exemple pour supprimer l'article relatif au FW-190 dans la table :

plane_tbl.delete { |r| r.name == 'FW-190' }

Important

Quand vous ne donnez aucun bloc de code à la méthode #select, KirbyBase sélectionne automatiquement tous les articles de la table. J'ai trouvé que faire de même pour la méthode #delete serait trop dangereux. Il serait trop facile de supprimer accidentellement tous les articles d'une table en oubliant de fournir un bloc de code à un #delete. C'est pourquoi la méthode #delete requiert un bloc de code. Pour supprimer tous les articles d'une table, utilisez la méthode #clear ou #delete_all.

Note

La méthode #delete retourne un entier indiquant le nombre d'articles supprimés.

La méthode clear (ou delete_all)

Pour vider entièrement une table, utilisez la méthode clear. Par exemple :

plane_tbl.clear

Important

Par défaut, KirbyBase remettra le compteur recno à 0. Aussi, tout nouvel article inséré dans la table après un #clear recevra un :recno commençant à 1. Ceci peut être évité en passant le paramètre false à #clear.

La méthode pack

Quand KirbyBase supprime un article, il remplit en fait la ligne d'espaces. En effet, supprimer la ligne et remonter toutes les suivantes d'une ligne prendrait trop de temps. Quand un article est modifié, si la taille de l'article modifié est plus grande que la taille de l'ancien article, KirbyBase remplit la ligne d'espaces et en écrit une nouvelle en fin de fichier. Ici encore, c'est pour éviter de récrire tout le fichier pour une seule ligne modifiée.

Évidemment, après un tas de suppression et mises à jour, une table peut contenir beaucoup de lignes blanches. Ceci ralentit les recherches et rend le fichier inutilement gros. Vous pouvez utiliser la méthode pack pour enlever ces lignes blanches :

result = plane_tbl.pack

Note

La méthode #pack rend un entier spécifiant le nombre de lignes blanches supprimées.

Champs memo et blob

Les champs mémos et blobs opèrent un peu différemment des autres. Vous les créez comme les autres. Vous devez indiquer le chemin de stockage des fichiers mémos ou blobs qui seront créés.

db.create_table(:plane, :name, :String, :speed, :Integer, :descr,
:Memo) do |d|
d.memo_blob_path = './memos'
end

Cependant, ce que vous stockez vraiment dans le champ mémo à l'occasion d'un #insert est une instance de KBMemo. KBMemo a deux attributs : :filepath et :contents. Le premier contient le chemin (y compris le nom de fichier) vers le fichier texte qui contient le texte du mémo. Ce chemin sera relatif au chemin qui a été spécifié dans le memo_blob_path à la création de la table. Voici un exemple :

memo_string = <<END_OF_STRING
The FW-190 was a World War II German fighter. It was used primarily as an
interceptor against Allied strategic bombers.
END_OF_STRING

memo = KBMemo.new(db, 'FW-190.txt', memo_string)
plane_tbl.insert('FW-190', 'Germany', 399, 499, memo)

Les mises à jour fonctionnent de façon similaire en ce sens que devez fournir une instance de KBMemo à la méthode #update pour le champ :Memo.

A part cette différence, vous utilisez un champ mémo comme un champ ordinaire. Quand vous faites un #select, KirbyBase va chercher le fichier qui contient les données du mémo, lit toutes les lignes, et rend une instance de KBMemo. Voici un exemple de requête impliquant un mémo :

plane_tbl.select { |r| r.descr.contents =~ /built in Detroit, Michigan/ }

et KirbyBase sélectionnera tous les articles dont le champ mémo contient "built in Detroit, Michigan".

Les champs Blob fonctionnent de la même façon, sauf qu'au lieu de faire un #readlines dessus, KirbyBase ouvre le fichier en mode binaire et le lit d'un coup.

Miscellaneous KirbyBase methods

Méthodes diverses de KirbyBase

KirbyBase#drop_table

db.drop_table(:plane)

supprime une table de la base de données. Rend true si la table a été supprimée.

KirbyBase#tables

db.tables

rend un tableau de tous les noms de tables de la base de données.

KirbyBase#table_exists?

db.table_exists?(:plane)

rend true si la table existe, false sinon.

KirbyBase#rename_table

db.rename_table(:plane, :warplanes)

change le nom de la table. Rend un handle sur la table renommée.

Diverses méthodes de KBTable

KBTable#[]=

plane_tbl[5] = {:country = 'Tasmania'}

Vous pouvez rapidement mettre à jour un article donné en traitant la table comme un hash dont la clé est le recno. Vous pouvez mettre à jour via un hash, un tableau, ou une struct.

Rend un entier indiquant le nombre d'articles modifiés (qui devrait toujours être 1).

KBTable#[]

plane_tbl[5]

Vous pouvez sélectionner rapidement un article en passant son recno à une table comme si c'était un hash.

Rend un seul article soit sous la forme d'une struct, soit d'une instance de la classe propre à l'article, si spécifiée.

plane_tbl[5, 14, 33]

Vous pouvez aussi utiliser la méthode [] pour récupérer un groupe d'articles en passant leurs recnos.

Rend un KBResultSet avec les articles ayant les recnos passés.

KBTable#field_names

plane_tbl.field_names

Rend un tableau des noms de champs de la table.

KBTable#field_types

plane_tbl.field_types

Rend un tableau des types des champs de la table (i.e. :String, :Integer, :Float)

KBTable#total_recs

plane_tbl.total_recs

Rend un entier donnant le nombre d'articles de la table.

KBTable#import_csv

plane_tbl.import_csv(csv_filename)

Cette méthode vous permet d'importer un fichier csv dans la table. KirbyBase tentera de convertir les valeurs du fichier csv en leur type de champ KirbyBase correspondant, en se basant sur les types définis lors de la création de la table.

Rend un entier indiquant le nombre d'articles importés.

KBTable#add_column

plane_tbl.add_column(:weight, :Integer, :range)

ajoute une colonne à une table existante.
Vous devez spécifier un nom de colonne, et un type. Vous pouvez en option spécifier la colonne après laquelle la nouvelle colonne doit être ajoutée. Si cela n'est pas indiqué, la nouvelle colonne sera la dernière.

Important

Comme #add_column change la structure de la table, vous ne pouvez appeler cette méthode que si vous avez connect_type==:local.

KBTable#drop_column

plane_tbl.drop_column(:speed)

supprime une colonne d'un table existante.
Vous devez indiquer le nom de la colonne à supprimer.

Important

Vous ne pouvez pas supprimer la colonne :recno.

Important

Comme #drop_column change la structure de la table, vous ne pouvez appeler cette méthode que si vous avez connect_type==:local.

KBTable#rename_column

plane_tbl.rename_column(:speed, :maximum_speed)

renomme une colonne dans une table existante.
Vous devez spécifier le nom de la colonne à renommer et son nouveau nom.

Important

Vous ne pouvez pas renommer la colonne :recno.

Important

Comme #rename_column change la structure de la table, vous ne pouvez appeler cette méthode que si vous avez connect_type==:local.

KBTable#change_column_type

plane_tbl.change_column_type(:weight, :Float)

change le type d'une colonne dans une table existante.
Vous devez spécifier le nom de la colonne et son nouveau type.

Important

Vous ne pouvez changer le type de la colonne :recno.

Important

Comme #change_column_type change la structure de la table, vous ne pouvez appeler cette méthode que si vous avez connect_type==:local.

KBTable#change_column_default_value

plane_tbl.change_column_default_value(:country, 'United States')

change la valeur par défaut d'une colonne dans une table existante.
Vous devez spécifier le nom de la colonne et une valeur par défaut. Si la valeur par défaut indiquée est nil, ceci supprimera la valeur par défaut de la colonne.

Important

La colonne :recno ne peut pas avoir de valeur par défaut, on ne peut donc pas la modifier.

Important

Comme #change_column_default_value change la structure de la table, vous ne pouvez appeler cette méthode que si vous avez connect_type==:local.

KBTable#change_column_required

plane_tbl.change_column_required(:country, true)

change le fait qu'une colonne soit obligatoire ou non.
Vous devez spécifier le nom de la colonne et true ou false.

Important

Vous ne pouvez spécifier que :recno soit requis ou non.

Important

Comme #change_column_required change la structure de la table, vous ne pouvez appeler cette méthode que si vous avez connect_type==:local.

KBTable#add_index

plane_tbl.add_index(:name, :country)

ajoute un index à une table existante.
Cet index peut consister en une ou plusieurs colonnes. Vous devez spécifier une ou plusieurs colonnes devant constituer l'index.

Important

Comme #add_index change la structure de la table, vous ne pouvez appeler cette méthode que si vous avez connect_type==:local.

KBTable#drop_index

plane_tbl.drop_index(:name, :country)

supprime un index d'une table existante.
Vous devez spécifier un ou plusieurs noms de colonnes qui définissent l'index à supprimer.

Important

Comme #drop_index change la structure de la table, vous ne pouvez appeler cette méthode que si vous avez connect_type==:local.

Les caractères spéciaux dans les données

Comme les tables KirbyBase sont de simples fichiers texte, avec marque de fin de ligne et les champs séparés par un caractère |, certains caractères ASCII peuvent poser des problèmes s'ils sont présents dans les données. Par exemple introduire une marque de fin de ligne (\n sous Unix, \r\n sous Windows) dans un article causera des problèmes plus tard, quand KirbyBase essaiera de lire cet article. De même, utiliser le caractère | dans les données causera des problèmes puisque KirbyBase se sert de ce caractère pour délimiter les champs. De plus, il s'avère qu'en Python on a des problèmes avec le code octal \032 sous Windows (peut-être parce que cela équivaut à un Ctrl-Z), alors pour garder la compatibilité entre les versions ruby et Python de KirbyBase, ce problème doit être géré.

Pour gérer ce problème de caractères spéciaux, KirbyBase vérifie toutes les données entrées en :String et :YAML et remplace les caractères spéciaux par des codages sans danger. Voici les remplacements qui sont effectués :

Caractère en entrée

KirbyBase le remplace par

\n

&amp;linefeed;

\r

&amp;carriage_return;

\032

&amp;substitute;

|

&amp;pipe;

&

&amp;

KirbyBase traduit les caractères à la lecture et à l'écriture dans la table. Ceci devrait être transparent pour l'utilisateur. Le seul moment où vous rencontrerez les remplacements sera celui où vous ouvrirez le fichier contenant la table avec un éditeur de texte, ou bien le lirez avec autre chose que KirbyBase.

Structure des tables

Chaque table de KirbyBase est représentée par un fichier physique de type texte, délimité, avec marques de fin de ligne. Voici un exemple :

000006|000000|Struct|recno:Integer|name:String|country:String|speed:Integer
1|P-51|USA|403
2|P-51|USA|365
3|Spitfire|England|345
4|Oscar|Japan|361
5|ME-109|Germany|366
6|Zero|Japan|377

La première ligne est l'article d'en-tête. Chaque champ est délimité par un "|". Le premier champ de l'article est le compteur d'articles. Il est incrémenté par KirbyBase pour créer de nouveaux numéros d'articles quand ceux-ci sont insérés dans la table.

Le second champ de l'en-tête est le compteur d'articles supprimés. Chaque fois qu'une ligne dans le fichier est remplie par des espaces (cf la méthode pack), ce nombre est incrémenté. Vous pouvez l'utiliser dans un script de maintenance pour compacter la table quand ce compteur atteint par exemple, 5000.

Le troisième champ de l'en-tête est la classe de l'article. Si vous avez spécifié une classe en créant la table, son nom apparaîtra ici et les articles rendus par un #select seront des instances de cette classe. Le défaut est "Struct".

Le quatrième champ de l'en-tête est le champ :recno. Ce champ est ajouté automatiquement à la table quand elle est créée. Le nom du champ et son type sont séparés par un ":".

Les autres champs de l'en-tête sont les champs que vous avez spécifiés à la création de la table.

S'il y a un Z dans la première position de l'en-tête et que le reste du fichier est un tas de caractères sans signification apparente, c'est que la table est cryptée.

Chaque article de la table est une ligne de texte, terminée par un délimiteur de ligne.

Notes sur le serveur

Il y a un script serveur dans la distribution, nommé kbserver.rb. Ce script utilise DRb pour transformer KirbyBase en un sgbd client/serveur, multi-utilisateur. Ce serveur gère les verrous sur les tables pour vous.

Conseils pour améliorer les performances

Attention à Date/DateTime

Convertir une chaîne de caractères (le format dans lequel les données sont stockées dans une table KirbyBase) en une donnée de type Date/DateTime est lent. Si vous avez une grande table avec un ou des champs de type Date/DateTime, les temps d'exécution des requêtes peuvent être très longs.

Pour éviter cela, vous pouvez spécifier le type de données en :String, au lieu de :Date/:DateTime. Les requêtes fonctionneront tout aussi bien, parce que les champs Date/DateTime qui sont au format String se trient de la même façon qu'en format natif. Voici un exemple. Les deux expressions suivantes donneront le même résultat :

date_field <= Date.new(2005, 05, 11)

et

date_field_stored_as_string_field <= "2005-05-11"

Créer des index sur de grandes tables

Plus une table est grande, plus il est intéressant de créer des index sur un ou plusieurs de ses champs. Même si cela entraîne une charge au moment où KirbyBase les crée, vous le rattrapez après par des vitesses très supérieures dans les requêtes. J'ai observé des accélérations d'un facteur 10 sur de grandes tables.

Créer des index sur des clés étrangères

Si vous avez des relations un-à-n établies, vous pouvez envisager de créer un index sur le champ dans la table fille vers quoi pointe le lien. Quand vous utiliserez le lien un-à-n, KirbyBase utilisera automatiquement cet index.

C'est la même chose pour les champs associés.

Quand c'est possible, cherchez avec :recno

Si vous pouvez utiliser le :recno pour vos requêtes, utilisez la méthode #select_by_recno_index (ou méthode #[] ). Ce sera encore plus rapide que de chercher via un index ordinaire, parce que les index du :recno que KirbyBase crée pour chaque table est un Hash, et non un tableau comme pour les index ordinaires.

Diagramme mémoire du mode Client/Serveur

images/client_server.png

Figure: Mode Client/Serveur – les espaces mémoire sont séparés.

Diagramme mémoire du mode mono-utilisateur

images/single_user.png

Figure: Mode mono-utilisateur.

Licence

KirbyBase est distribué selon la même licence que ruby.

Crédits

Note

Ce manuel a été produit avec le très sympa formateur de documents texte, AsciiDoc. Et la traduction avec l'éditeur HTML d'OpenOffice.org V 1.1.4