Python

Mon but était de scripter le DDL d'un serveur SQL entier (tables, vues, procédures, sécurité…) pour le stocker dans une arborescence de répertoires sur le disque. Cela me permet de le conserver dans un outil de gestion de version (CVS, subversion, au pire Sourcesafe). Afin d'assurer une certaine compatibilité avec un groupe d'outils très puissants pour déployer des structures de bases à partir de projets Sourcesafe, et développé en Perl : AbaPerls, j'ai adopté une certaine convention de nommage des objets, séparant par exemple pour les tables la création de la table elle-même de ses contraintes d'intégrité référentielle et de ses indexes. Cela permet de générer une base sans se préoccuper de l'ordre de création des objets, puisque les tables peuvent être toutes générées d'abord, et toutes les contraintes DRI appliquées ensuite.

La première réécriture de mon script Perl original (moins sophistiqué) fut faite en Python. La réalisation est très simple, l'importation du DMO et des constantes de la bibliothèque sont aussi aisé que dans un langage M$. Le seul problème qui demeure à l'heure actuelle est un problème de jeu de caractères en scriptant des objets dont le code comporte des signes diacritiques (des accents dans les commentaires, principalement). Python génère une erreur. J'ai essayé de trouver un moyen de changer l'encodage, pour l'instant sans succès.

Une autre déception est venue lorsque, dans le code appelant ma classe, j'ai voulu utiliser un fichier de configuration XML. En Perl, j'utiliserais XML::simple et en un clin d'oeil ma structure XML serait disponible en tableaux associatifs. Apparemment le support d'XML est actuellement un point noir des bibliothèques Python. Pour y pallier, j'ai utilisé ConfigObj qui lit et écrit un fichier ini hiérarchique.

Ruby

Réécrire mon script Python en Ruby a pris très peu de temps : supprimer les : à la fin des structures de contrôle, finir le bloc avec un END, changer les appels de bibliothèques, quelques opérateurs, la syntaxe des expressions rationnelles. Le résultat donne un script très proche de son équivalent Python, à tel point qu'un diff devrait donner des différences minimales. Mais plus de double soulignements et d'indentation obligatoire. Ouf !

Une nuance toutefois : il semble qu'il n'y ait pas de destructeur en Ruby. Le principe est que de toute manière la classe et ses ressources vont être nettoyées par le garbage collector. J'avais en Python placé la déconnexion au serveur SQL dans un del. En Ruby j'ai créé une méthode disconnect. J'ai vu des propositions de méthodes détournées pour implémenter une façon de destructeur, mais je n'ai pas encore bien plongé dans le sujet.

Pour utiliser un fichier de configuration, je suis vite tombé sur yaml4r, une implémentation de YAML pour Ruby. Je ne connaissais pas YAML, dont le nom est encore un acronyme récursif (YAML Ain't a Markup Language) alors qu'il joue sur la construction habituelle du Yet Another… qui pourrait donner Yet Another Markup Language. C'est une initiative pour définir un langage très simple de description de contenu multiusage, à la fois descriptif et peu bavard (par opposition au XML). Il permet de structurer tout type d'information, aussi bien des tableaux, des listes, des logs qu'une sérialisation d'objets, sous une forme claire et très simple à comprendre. Ainsi on obtient par exemple un fichier de configuration hiérarchique aisé à maintenir et à lire, et parsable sans effort en Ruby. yaml4r fait maintenant partie des bibliothèques officielles livrées avec la distribution de Ruby. Vous n'aurez par besoin de les installer pour les utiliser.

Code Python

extract.py

#
# requires ConfigObj : http://www.voidspace.org.uk/python/configobj.html
#
from win32com.client import constants, gencache
import win32api
import datetime, os, re, sys, locale 
from configobj import ConfigObj
 
# avoid problems with diacritics. ascii is default encoding in Python
#loc = locale.getdefaultlocale()
#if loc[1]:
#    sys.setdefaultencoding(loc[1])
 
 
class extract:
	# == params ==
	IncludeCollation = False
 
	def __init__(self):
		self.s = gencache.EnsureDispatch('SQLDMO.SQLServer') # we put it here to have COM enum (constants) available
		# == globals params ==
		self.opToFile = constants.SQLDMOScript_ToFileOnly # 64
		self.opScript = constants.SQLDMOScript_PrimaryObject | constants.SQLDMOScript_ObjectPermissions | constants.SQLDMOScript_OwnerQualify
		self.opTbl = constants.SQLDMOScript_Indexes | self.opScript
		self.op2 = constants.SQLDMOScript2_AnsiFile
		if not self.IncludeCollation:
			self.op2 = self.op2 | constants.SQLDMOScript2_NoCollation 
 
	def __del__(self):
		self.s.DisConnect()
 
	def connect(self, server, integrated = True, login = None, pwd = None):
		self.s.LoginSecure = integrated
		self.s.Connect(server, login, pwd)
 
	# script all server objects
	def scriptServer(self, curdir='.'):
		serverdir = curdir + '\\' + self.s.Name + '\\'
		os.mkdir(serverdir)
		for db in self.s.Databases:
			print "scripting database " + db.Name
			if not db.SystemObject:
				dbdir = serverdir+'\\'+db.Name
				os.mkdir(dbdir)
 
				# -- tables --
				os.mkdir(dbdir+'\\tbl')
				for tbl in db.Tables:
					if not tbl.SystemObject:
						script = tbl.Script(constants.SQLDMOScript_ToFileOnly | self.opTbl, dbdir+'\\tbl\\'+ tbl.Name +".tbl.sql", None, self.op2)
 
						# -- foreign keys --
						fkeys = ''
						for fk in tbl.Keys:
							if fk.Type == 3: #SQLDMOKey_Foreign
								fkeys += fk.Script(self.opTbl, None, self.op2)
 
						if len(fkeys) > 0:
							fkeys = re.sub('\r', '', fkeys) # il fait des linefeeds...
							h = open(dbdir+'\\tbl\\'+ tbl.Name +".fkey.sql", 'w')
							h.write(fkeys)
							h.close()
 
						# -- indexes --
						indexes = ''
						for idx in tbl.Indexes:
							indexes += idx.Script(self.opTbl, None, self.op2)
 
						if len(indexes) > 0:
							indexes = re.sub('\r', '', indexes) # il fait des linefeeds...
							h = open(dbdir+'\\tbl\\'+ tbl.Name +".ix.sql", 'w')
							h.write(indexes)
							h.close()
 
						# -- triggers --
						triggers = ''
						for tri in tbl.Triggers:
							triggers += tri.Script(self.opTbl, None, self.op2)
 
						if len(triggers) > 0:
							triggers = re.sub('\r', '', triggers) # il fait des linefeeds...
							h = open(dbdir+'\\tbl\\'+ tbl.Name +".tri.sql", 'w')
							h.write(triggers)
							h.close()
				# -- views --	
				os.mkdir(dbdir+'\\view')
				for vw in db.Views:
					if not vw.SystemObject:
						script = vw.Script(self.opToFile | self.opTbl, dbdir+'\\view\\'+ vw.Name +".view.sql")
 
				# -- sp --	
				os.mkdir(dbdir+'\\sp')
				for sp in db.StoredProcedures:
					if not sp.SystemObject:
						script = sp.Script(self.opToFile | self.opScript, dbdir+'\\sp\\'+ sp.Name +".sp.sql")
 
				# -- udf --	
				os.mkdir(dbdir+'\\functions')
				for udf in db.UserDefinedFunctions:
					if not udf.SystemObject:
						script = udf.Script(self.opToFile | self.opScript, dbdir+'\\functions\\'+ udf.Name +".sqlfun.sql")
 
				# -- security --	
				os.mkdir(dbdir+'\\security')
				for user in db.Users:
					if not user.SystemObject:
						username = re.sub(r'\\', '_', user.Name) # attention au backslashs...
						script = user.Script(self.opToFile | self.opScript, dbdir+'\\security\\'+ username +".user.sql")
 
				for role in db.DatabaseRoles:
					if not role.IsFixedRole:
						script = role.Script(self.opToFile | self.opScript, dbdir+'\\security\\'+ role.Name +".role.sql")
 
				# -- datatypes --	
				os.mkdir(dbdir+'\\datatypes')
				for type in db.UserDefinedDatatypes:
					script = type.Script(self.opToFile | self.opScript, dbdir+'\\datatypes\\'+ type.Name +".datatype.sql")
 
	# generate INSERT statements for a table
	#def generateInserts(s, curdir='.', tableName, includeIdentity = false, top = 0):

Code Ruby

sqltools/extract.rb

require 'win32ole'
 
module DMO
end
 
class Extract
	# == params ==
	@@IncludeCollation = false
 
	def initialize()
		@s = WIN32OLE.new('SQLDMO.SQLServer')
		WIN32OLE.const_load(@s, DMO)
		# == globals params ==
		@opToFile = DMO::SQLDMOScript_ToFileOnly # 64
		@opScript = DMO::SQLDMOScript_PrimaryObject | DMO::SQLDMOScript_ObjectPermissions | DMO::SQLDMOScript_OwnerQualify
		@opTbl = DMO::SQLDMOScript_Indexes | @opScript
		@op2 = DMO::SQLDMOScript2_AnsiFile
		if not @IncludeCollation
			@op2 = @op2 | DMO::SQLDMOScript2_NoCollation 
		end
	end
 
	#def __del__(self):
	#	@s.DisConnect()
 
	def connect(server, integrated = true, login = nil, pwd = nil)
		@s.LoginSecure = integrated;
        @s.Connect(server, login, pwd);
    end
 
	def disconnect
        @s.DisConnect()
	end
 
	# script all server objects
	def scriptServer(curdir='.')
		$stdout.print "scripting server " + @s.Name + "\n\n"
		serverdir = curdir + '\\' + @s.Name + '\\'
		Dir.mkdir(serverdir)
		for db in @s.Databases
			if (not db.SystemObject) && (db.Status == DMO::SQLDMODBStat_Normal)
				$stdout.print "scripting database " + db.Name + "\n"
				dbdir = serverdir+'\\'+db.Name
				Dir.mkdir(dbdir)
 
				# -- tables --
				Dir.mkdir(dbdir+'\\tbl')
				for tbl in db.Tables
					if not tbl.SystemObject
						script = tbl.Script(DMO::SQLDMOScript_ToFileOnly | @opTbl, dbdir+'\\tbl\\'+ tbl.Name() +'.tbl.sql', nil, @op2)
 
						# -- foreign keys --
						fkeys = ''
						for fk in tbl.Keys
							if fk.Type == DMO::SQLDMOKey_Foreign
								fkeys += fk.Script(@opTbl, nil, @op2)
							end
						end
 
						if fkeys.length > 0
							fkeys = fkeys.sub(/\r/,'')
							h = open(dbdir+'\\tbl\\'+ tbl.Name() +".fkey.sql", 'w')
							h.write(fkeys)
							h.close()
						end
 
						# -- indexes --
						indexes = ''
						for idx in tbl.Indexes
							indexes += idx.Script(@opTbl, nil, @op2)
						end
 
						if indexes.length > 0
							indexes = indexes.sub(/\r/, '')
							h = open(dbdir+'\\tbl\\'+ tbl.Name() +".ix.sql", 'w')
							h.write(indexes)
							h.close()
						end
 
						# -- triggers --
						triggers = ''
						for tri in tbl.Triggers
							triggers += tri.Script(@opTbl, nil, @op2)
						end
 
						if triggers.length > 0
							triggers = triggers.sub(/\r/, '')
							h = open(dbdir+'\\tbl\\'+ tbl.Name() +".tri.sql", 'w')
							h.write(triggers)
							h.close()
						end
					end
				end
 
				# -- views --	
				Dir.mkdir(dbdir+'\\view')
				for vw in db.Views
					if not vw.SystemObject
						script = vw.Script(@opToFile | @opTbl, dbdir+'\\view\\'+ vw.Name() +".view.sql")
					end
				end
 
				# -- sp --	
				Dir.mkdir(dbdir+'\\sp')
				for sp in db.StoredProcedures
					if not sp.SystemObject
						script = sp.Script(@opToFile | @opScript, dbdir+'\\sp\\'+ sp.Name() +".sp.sql")
					end
				end
 
				# -- udf --	
				Dir.mkdir(dbdir+'\\functions')
				for udf in db.UserDefinedFunctions
					if not udf.SystemObject
						script = udf.Script(@opToFile | @opScript, dbdir+'\\functions\\'+ udf.Name() +".sqlfun.sql")
					end
				end
 
				# -- security --	
				Dir.mkdir(dbdir+'\\security')
				for user in db.Users
					if not user.SystemObject
						username = user.Name()
						username = username.sub(/\\/, '_')
						script = user.Script(@opToFile | @opScript, dbdir+'\\security\\'+ username +".user.sql")
					end
				end
 
				for role in db.DatabaseRoles
					if not role.IsFixedRole
						script = role.Script(@opToFile | @opScript, dbdir+'\\security\\'+ role.Name() +".role.sql")
					end
				end
 
				# -- datatypes --	
				Dir.mkdir(dbdir+'\\datatypes')
				for type in db.UserDefinedDatatypes
					script = type.Script(@opToFile | @opScript, dbdir+'\\datatypes\\'+ type.Name() +".datatype.sql")
				end
			end
		end
	end
end
 
	# generate INSERT statements for a table
	#def generateInserts(s, curdir='.', tableName, includeIdentity = false, top = 0):

Code d'appel de la classe Ruby

scriptDatabase.rb

#
# I'm using a config file in YAML format : http://www.yaml.org/
#
require "sqltools/extract"
require 'yaml'
 
conf = YAML::load( File.open( 'config.yaml' ) )
 
# create folder
curdir = '.\\' + Time.now().strftime("%Y-%m-%d.%H%M")
Dir.mkdir(curdir)
 
e = Extract.new()
 
for s in conf
    if conf[s[0]]['integrated'] == 1
        e.connect(conf[s[0]]['host'])
    else
        e.connect(conf[s[0]]['host'], false, conf[s[0]]['login'], conf[s[0]]['pwd'])
    end
    e.scriptServer(curdir)
    e.disconnect()
end

Exemple de fichier de configuration :

config.yaml

server1: 
 host: 127.0.0.1 
 integrated: 0
 login: mylogin
 pwd: mypassword
 
server2: 
 host: localhost
 integrated: 1

Le code est téléchargeable :

 
programmation/comparaison_python-ruby.txt · Dernière modification: 2006/03/29 22:19 (édition externe)