No Description
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

sqlwrapper.py 18KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434
  1. # -*- coding: utf-8 -*-
  2. from sqlalchemy import * # TODO ajuster les classes à importer
  3. from Database.sqlsettings import SQLSettings as sqlsettings
  4. from django.conf import settings
  5. import re
  6. class SqlWrapper(object):
  7. def __init__(self):
  8. # TODO Passer ces deux éléments dans les settings django (au niveau de l'init de l'appli)
  9. self.read_engine = self.get_engine(sqlsettings.DB_READ_CONNECTION_NAME)
  10. self.write_engine = self.get_engine(sqlsettings.DB_WRITE_CONNECTION_NAME)
  11. def get_engine(self, connection_name):
  12. """ Crée un moteur logique sur une base de données
  13. @param connection_name str: Connection name as defined is django settings
  14. @return Engine
  15. """
  16. connection_params = settings.DATABASES[connection_name]
  17. dialect = None
  18. for dbms in sqlsettings.dbms_list:
  19. if dbms in connection_params['ENGINE']:
  20. dialect=dbms
  21. break
  22. driver = sqlsettings.dbms_list[dialect]['driver']
  23. username = connection_params['USER']
  24. password = connection_params['PASSWORD']
  25. hostname = connection_params['HOST'] if 'HOST' in connection_params else sqlsettings.DEFAULT_HOSTNAME
  26. port = connection_params['PORT'] if 'PORT' in connection_params else ''
  27. host = hostname if port=='' else '%s:%s' % (hostname, port)
  28. database = connection_params['NAME']
  29. connection_string = '%s+%s://%s:%s@%s/%s' % (dialect, driver, username, password, host, database)
  30. engine = create_engine(connection_string, encoding=sqlsettings.dbms_list[dialect]['encoding'], echo=True)
  31. return engine
  32. def get_read_engine(self):
  33. return self.read_engine
  34. def get_write_engine(self):
  35. return self.write_engine
  36. def execute(self, queries, action_type):
  37. """ Exécute une série de requêtes en base
  38. @param queries list: une liste de requêtes
  39. @param action_type str: le type d'action ( 'read'|'write' )
  40. @return List. Tableau de résultats sous forme de tuples (requête, résultat)
  41. """
  42. db = self.get_write_engine() if action_type==sqlsettings.ACTION_TYPE_WRITE else self.get_read_engine()
  43. connection = db.connect()
  44. transaction= connection.begin()
  45. result = []
  46. queries = queries if isinstance(queries, list) else [queries]
  47. try:
  48. for query in queries:
  49. result.append((query, connection.execute(query)))
  50. transaction.commit()
  51. connection.close()
  52. except:
  53. transaction.rollback()
  54. connection.close()
  55. result = None
  56. raise
  57. return result
  58. def create_foreign_key_constraint_object(self, localcol, remotecol, constraint_name=None):
  59. """ Génère une contrainte Clé étrangère
  60. @param localcol str: Colonne locale
  61. @param remotecol str: Colonne distante (syntaxe : Table.col)
  62. @param constraint_name str: Nom de la contrainte (par défaut : fk_localcol_remotecol)
  63. Returns:
  64. ForeignKeyConstraint.
  65. """
  66. foreignkeyobj = ForeignKeyConstraint([localcol], [remotecol], name=constraint_name)
  67. return foreignkeyobj
  68. def create_column_object(self, column_name, column_type, column_extra=None):
  69. """ Génère un objet colonne
  70. Exemple de dictionnaire pour column_extra : { "primarykey":True, "nullable":False, "default":"test" ... }
  71. @param column_name str: Nom de la colonne
  72. @param column_type str: Type de la colonne
  73. @param column_extra dict : Objet json contenant les paramètres optionnels comme PRIMARY KEY, NOT NULL, etc ...
  74. @return Column. La méthode renvoie "None" si l'opération échoue
  75. """
  76. # Traitement du type (mapping avec les types SQLAlchemy)
  77. # TODO créer une méthode qui fait un mapping plus complet
  78. column = None
  79. if column_type=='INTEGER':
  80. column = Column(column_name, INTEGER)
  81. elif 'VARCHAR' in column_type:
  82. check_length = re.search(re.compile('VARCHAR\(([\d]+)\)', re.IGNORECASE), column_type)
  83. column_length = int(check_length.groups()[0]) if check_length else None
  84. column = Column(column_name, VARCHAR(length=column_length))
  85. elif column_type=='TEXT':
  86. column = Column(column_name, TEXT)
  87. elif column_type=='DATE':
  88. column = Column(column_name, DATE)
  89. elif column_type=='BOOLEAN':
  90. column = Column(column_name, BOOLEAN)
  91. if column is not None and column_extra:
  92. if 'nullable' in column_extra:
  93. column.nullable = column_extra['nullable']
  94. if 'primarykey' in column_extra:
  95. column.primary_key = column_extra['primarykey']
  96. if 'default' in column_extra:
  97. column.default = column_extra['default']
  98. if 'foreingkey' in column_extra:
  99. column.append_foreign_key(ForeignKey(column_extra['foreignkey']))
  100. return column
  101. def create_table(self, tableparams):
  102. """ Crée une nouvelle table
  103. Les paramètres de la tables sont passés via un dictionnaire dont voici la description :
  104. { 'name':'<nom_table>', 'columns': [ <col1>, <col2>, <coln> ], 'constraints': dict(...) }
  105. Avec <colX> : { 'name': '<name_colX>', 'type': '<type_colX>', 'extra': dict(...) }
  106. A noter que les types de col (<type_colX>) sont des chaines representant un type SQL (ex: "VARCHAR(50)")
  107. Les deux champs "name" et "columns" seulement sont obligatoires.
  108. Le champ "extra" de la définition des colonnes est facultatif.
  109. @args tableparams dict: Dictionnaire avec les paramétrages de la table
  110. @return bool. True si le process est allé au bout, False si on a rencontré une erreur.
  111. """
  112. metadata = MetaData()
  113. table = Table(tableparams['name'], metadata)
  114. columns = tableparams['columns']
  115. for column in columns:
  116. column_extra = column['extra'] if 'extra' in column else None
  117. table.append_column(self.create_column_object(column['name'], column['type'], column_extra))
  118. try:
  119. table.create(self.get_write_engine())
  120. return True
  121. except:
  122. # TODO Ajuster le code d'erreur à retourner
  123. return False
  124. def get_table(self, table_name, action_type=sqlsettings.ACTION_TYPE_WRITE):
  125. """ Récupère une table dans un objet Table
  126. @param table_name str: Nom de la table
  127. @param action_type str: Type d'action (read|write) (par défaut : "write")
  128. @return Table.
  129. """
  130. db = self.get_write_engine() if action_type == sqlsettings.ACTION_TYPE_WRITE else self.get_read_engine()
  131. metadata = MetaData()
  132. return Table(table_name, metadata, autoload=True, autoload_with=db)
  133. def drop_table(self, table_name):
  134. """ Supprime une table
  135. @param table_name str: Nom de la table
  136. @return bool. True si le process est allé au bout. False si on a rencontré une erreur
  137. """
  138. try:
  139. db = self.get_write_engine().connect()
  140. metadata = MetaData()
  141. table = Table(table_name, metadata, autoload=True, autoload_with=db)
  142. # Pour le drop, on utilise checkfirst qui permet de demander la suppression préalable des contraintes liées à la table
  143. table.drop(db, checkfirst=True)
  144. db.close()
  145. return True
  146. except:
  147. # TODO ajuster le code d'erreur
  148. return False
  149. def get_querystring(self, action, dialect):
  150. string_dialect = dialect if dialect in sqlsettings.querystrings[action] else 'default'
  151. querystring = sqlsettings.querystrings[action][string_dialect]
  152. return querystring
  153. def add_column(self, table_name, column):
  154. """ Ajoute une colonne à une table existante
  155. @param table_name str: nom de la table
  156. @param column dict: la colonne - {"name":"<nom de la colonne>", "type":"<type de la colonne>"}
  157. @return bool. True si le process est allé au bout, False si on a rencontré une erreur
  158. """
  159. sqlquery = self.get_querystring('add_column', self.get_write_engine().dialect) % (table_name, column['name'], column['type'])
  160. sqlresult = self.execute(sqlquery, sqlsettings.ACTION_TYPE_WRITE)
  161. return True if sqlresult else False
  162. def alter_column(self, table_name, column):
  163. """ Modifie le type d'une colonne
  164. @param table_name str: nom de la table
  165. @param column_name dict: la colonne - {"name":"<nom de la colonne>","type":"<nouveau type de la colonne>"}
  166. @return bool. True si le process est allé au bout. False si on a rencontré une erreur
  167. """
  168. sqlquery = self.get_querystring('alter_column', self.get_write_engine().dialect) % (table_name, column['name'], column['type'])
  169. sqlresult = self.execute(sqlquery, sqlsettings.ACTION_TYPE_WRITE)
  170. return True if sqlresult else False
  171. def insert(self, table_name, newrecord):
  172. """ Insère un nouvel enregistrement
  173. @param table_name str: nom de la table
  174. @param newrecord dict: dictionnaire contenant les informations sur le nouvel enregistrement à insérer - {"column1":"value1", "column2":"value2", etc ...)
  175. @return int. Nombre de lignes insérées
  176. """
  177. sqlresult = self.execute(self.get_table(table_name).insert().values(newrecord), sqlsettings.ACTION_TYPE_WRITE)
  178. return sqlresult.rowcount
  179. def delete(self, table_name, whereclauses):
  180. """ Supprime un enregistrement
  181. @param table_name str: nom de la table
  182. @param whereclauses list: liste des conditions sur les enregistrements (sous forme de textes SQL)
  183. @return int. Nombre de lignes supprimées
  184. """
  185. deleteobject = self.get_table(table_name).delete()
  186. for whereclause in whereclauses:
  187. deleteobject = deleteobject.where(whereclause)
  188. sqlresult = self.execute(deleteobject, sqlsettings.ACTION_TYPE_WRITE)
  189. return sqlresult.rowcount
  190. def update(self, table_name, whereclauses, newvalues):
  191. """ Met à jour des enregistrements
  192. @param table_name str: nom de la table
  193. @param whereclauses list: liste des conditions sur les enregistrements (sous forme de textes SQL)
  194. @param newvalues dict: dictionnaire contenant les nouvelles valeurs à insérer - {"colonne1":"valeur1", "colonne2":"valeur2", etc ...}
  195. @return int. Nombre de lignes modifiées
  196. @throw DataError. Incompatibilité entre la valeur insérée et le type de colonne dans la table
  197. """
  198. table = self.get_table(table_name)
  199. update_object = table.update()
  200. updated_lines_count = 0
  201. try:
  202. for whereclause in whereclauses:
  203. update_object = update_object.where(whereclause)
  204. update_object = update_object.values(newvalues)
  205. sqlresult = self.execute(update_object, sqlsettings.ACTION_TYPE_WRITE)
  206. updated_lines_count = sqlresult.rowcount
  207. except DataError:
  208. # TODO Voir si on garde "-1" ou si on place un "None" ou un "False"
  209. updated_lines_count = -1
  210. return updated_lines_count
  211. def select(self, select_params):
  212. """ Récupère un jeu d'enregistrements
  213. Champs de select_params :
  214. - "what":"<liste des colonnes>",
  215. - "from":"<liste des tables>",
  216. - "where":"<liste des conditions>",
  217. - "distinct":True|False,
  218. - "join":{"left":"table_de_gauche.champ","right":"table_de_droite.champ", "options":{"outer":True|False}}
  219. - ...
  220. Les listes sont supportées sous deux formats :
  221. - texte SQL : "colonne1, colonne2, ..."
  222. - liste Python : ["colonne1", "colonne2", ...]
  223. @param select_params list: liste de dictionnaire permettant de caractériser la requête.
  224. @return list. Liste des dictionnaires représentant les différents enregistrements.
  225. """
  226. if not 'what' in select_params or not 'from' in select_params:
  227. # TODO Lever une exception à ce niveau
  228. raise
  229. query_what = select_params['what'] if isinstance(select_params['what'], list) else select_params['what'].replace(' ', '').split(',')
  230. query_from = select_params['from'] if isinstance(select_params['from'], list) else select_params['from'].replace(' ', '').split(',')
  231. query_where= select_params['where'] if isinstance(select_params['where'], list) else select_params['where'].replace(' ', '').split(',')
  232. query_distinct = select_params['distinct'] if 'distinct' in select_params else False
  233. query_limit = select_params['limit'] if 'limit' in select_params else False
  234. query_offset = select_params['offset'] if 'offset' in select_params else False
  235. query_orderby = select_params['order_by'] if 'order_by' in select_params else False
  236. query_groupby = select_params['group_by'] if 'group_by' in select_params else False
  237. query_having = select_params['having'] if 'having' in select_params else False
  238. columns_list = []
  239. if len(query_from) > 1:
  240. for column_id in query_what:
  241. # Il y a plusieurs tables, les colonnes sont donc nommées sur le format suivant : <nom de la table>.<nom du champ>
  242. column_id_array = column_id.split('.')
  243. columns_list.append(getattr(self.get_table(column_id_array[0]).c, column_id_array[1]))
  244. else:
  245. table = self.get_table(query_from[0])
  246. select_object = select(columns=columnslist)
  247. selected_lines = []
  248. try:
  249. # Traitement des différents paramètres de la requête
  250. for query_where_item in query_where:
  251. select_object = select_object.where(query_where_item)
  252. select_object = select_object.distinct(True) if query_distinct else select_object
  253. select_object = select_object.limit(query_limit) if query_limit else select_object
  254. select_object = select_object.offset(query_offset) if query_offset else select_object
  255. if query_orderby:
  256. for query_orderby_column, query_orderby_direction in query_orderby:
  257. select_object = select_object.order_by("%s %s" % (query_orderby_column, query_orderby_direction))
  258. #Traitement de la jointure
  259. if 'join' in select_params:
  260. select_join = self.create_join_object(select_params['join'])
  261. if select_join:
  262. select_object = select_object.select_from(select_join)
  263. else:
  264. raise # TODO Ajuster l'exception à lever (ici :"Données de jointure invalides ou manquantes")
  265. #Traitement du group_by
  266. if query_groupby:
  267. for group_by_clause in query_groupby:
  268. select_object = select_object.group_by(self.get_table_column_from_sql_string(group_by_clause))
  269. # Traitement du having
  270. if query_groupby and query_having:
  271. for having_clause in query_having:
  272. select_object = select_object.having(having_clause)
  273. # Exécution de la requête
  274. sqlresult = self.execute(select_object, sqlsettings.ACTION_TYPE_READ)
  275. # Transformation du résultat en une liste de dictionnaires
  276. records = sqlresult.fetchall()
  277. for record in records:
  278. selected_lines.append(dict(zip(record.keys(), record)))
  279. except:
  280. selected_lines = None
  281. return selected_lines
  282. def get_table_column_from_sql_string(self,sql_string):
  283. sql_string_array = sql_string.split('.')
  284. table_name = sql_string_array[0]
  285. column_name = sql_string_array[1]
  286. table_object = self.get_table(table_name,sqlsettings.ACTION_TYPE_READ)
  287. column_object = getattr(table_object.c, column_name)
  288. return column_object
  289. def create_join_object(self, join_params):
  290. """Génère un objet "Jointure" pour le greffer dans une requête
  291. Format des dictionnaires de description des jointures :
  292. - Format1 liste de champs
  293. - "left":"table.champ",
  294. - "right":"table.champ",
  295. - "options":{"outer":True|False}
  296. - Format2 liste de champs
  297. - "left":{"table":"nom de la table","field":"nom du champ"},
  298. - "right":{"table":"nom de la table","field":"nom du champ"},
  299. - "options":{"outer":True|False}
  300. @param join_params (dict) : dictionnaire de données sur la jointure.
  301. @return join.
  302. """
  303. join_object = None
  304. if 'left' in join_params and 'right' in join_params:
  305. if isinstance(join_params['left'], dict):
  306. join_left_table_name = join_params['left']['table']
  307. join_left_field_name = join_params['left']['field']
  308. else:
  309. join_left_array = join_params['left'].split('.')
  310. join_left_table_name = join_left_array[0]
  311. join_left_field_name = join_left_array[1]
  312. if isinstance(join_params['right'], dict):
  313. join_right_table_name = join_params['right']['table']
  314. join_right_field_name = join_params['right']['field']
  315. else:
  316. join_right_array = join_params['right'].split('.')
  317. join_right_table_name = join_right_array[0]
  318. join_right_field_name = join_right_array[1]
  319. left_table = self.get_table(join_left_table_name, sqlsettings.ACTION_TYPE_READ)
  320. left_field = getattr(left_table.c, join_left_field_name)
  321. right_table = self.get_table(join_right_table_name, sqlsettings.ACTION_TYPE_READ)
  322. right_field = getattr(right_table.c, join_right_field_name)
  323. join_option_isouter = True if 'options' in join_params and 'outer' in join_params and join_params['outer']==True else False
  324. join_object = join(left_table, right_table, left_field == right_field,isouter=join_option_isouter)
  325. return join_object