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 19KB

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