database.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311
  1. # -*- coding: utf-8 -*-
  2. """Database module, including the SQLAlchemy database object and DB-related utilities."""
  3. # flake8: noqa # flake8 has real problems linting this file on Python 2
  4. from pprint import pformat
  5. from sqlalchemy import desc, or_
  6. from sqlalchemy.sql.sqltypes import Date, DateTime
  7. from werkzeug import cached_property
  8. from flask import current_app
  9. from walle.service.extensions import db
  10. from walle.service.utils import basestring
  11. from walle.service.utils import datetime_str_to_obj, date_str_to_obj
  12. # Alias common SQLAlchemy names
  13. Column = db.Column
  14. relationship = db.relationship
  15. OPERATOR_FUNC_DICT = {
  16. '=': (lambda cls, k, v: getattr(cls, k) == v),
  17. '==': (lambda cls, k, v: getattr(cls, k) == v),
  18. 'eq': (lambda cls, k, v: getattr(cls, k) == v),
  19. '!=': (lambda cls, k, v: getattr(cls, k) != v),
  20. 'ne': (lambda cls, k, v: getattr(cls, k) != v),
  21. 'neq': (lambda cls, k, v: getattr(cls, k) != v),
  22. '>': (lambda cls, k, v: getattr(cls, k) > v),
  23. 'gt': (lambda cls, k, v: getattr(cls, k) > v),
  24. '>=': (lambda cls, k, v: getattr(cls, k) >= v),
  25. 'gte': (lambda cls, k, v: getattr(cls, k) >= v),
  26. '<': (lambda cls, k, v: getattr(cls, k) < v),
  27. 'lt': (lambda cls, k, v: getattr(cls, k) < v),
  28. '<=': (lambda cls, k, v: getattr(cls, k) <= v),
  29. 'lte': (lambda cls, k, v: getattr(cls, k) <= v),
  30. 'or': (lambda cls, k, v: or_(getattr(cls, k) == value for value in v)),
  31. 'in': (lambda cls, k, v: getattr(cls, k).in_(v)),
  32. 'nin': (lambda cls, k, v: ~getattr(cls, k).in_(v)),
  33. 'like': (lambda cls, k, v: getattr(cls, k).like('%%%s%%' % (v))),
  34. 'nlike': (lambda cls, k, v: ~getattr(cls, k).like(v)),
  35. '+': (lambda cls, k, v: getattr(cls, k) + v),
  36. 'incr': (lambda cls, k, v: getattr(cls, k) + v),
  37. '-': (lambda cls, k, v: getattr(cls, k) - v),
  38. 'decr': (lambda cls, k, v: getattr(cls, k) - v),
  39. }
  40. def parse_operator(cls, filter_name_dict):
  41. """ 用来返回sqlalchemy query对象filter使用的表达式
  42. Args:
  43. filter_name_dict (dict): 过滤条件dict
  44. {
  45. 'last_name': {'eq': 'wang'}, # 如果是dic使用key作为操作符
  46. 'age': {'>': 12}
  47. }
  48. Returns:
  49. binary_expression_list (lambda list)
  50. """
  51. def _change_type(cls, field, value):
  52. """ 有些表字段比如DateTime类型比较的时候需要转换类型,
  53. 前端传过来的都是字符串,Date等类型没法直接相比较,需要转成Date类型
  54. Args:
  55. cls (class): Model class
  56. field (str): Model class field
  57. value (str): value need to compare
  58. """
  59. field_type = getattr(cls, field).type
  60. if isinstance(field_type, Date):
  61. return date_str_to_obj(value)
  62. elif isinstance(field_type, DateTime):
  63. return datetime_str_to_obj(value)
  64. else:
  65. return value
  66. binary_expression_list = []
  67. for field, op_dict in list(filter_name_dict.items()):
  68. for op, op_val in list(op_dict.items()):
  69. op_val = _change_type(cls, field, op_val)
  70. if op in OPERATOR_FUNC_DICT:
  71. binary_expression_list.append(
  72. OPERATOR_FUNC_DICT[op](cls, field, op_val)
  73. )
  74. return binary_expression_list
  75. class CRUDMixin(object):
  76. """Mixin that adds convenience methods for
  77. CRUD (create, read, update, delete) operations."""
  78. @classmethod
  79. def create(cls, **kwargs):
  80. """Create a new record and save it the database."""
  81. instance = cls(**kwargs)
  82. return instance.save()
  83. @classmethod
  84. def create_from_dict(cls, d):
  85. """Create a new record and save it the database."""
  86. assert isinstance(d, dict)
  87. instance = cls(**d)
  88. return instance.save()
  89. def update(self, commit=True, **kwargs):
  90. """Update specific fields of a record."""
  91. for attr, value in list(kwargs.items()):
  92. setattr(self, attr, value)
  93. return commit and self.save() or self
  94. def save(self, commit=True):
  95. """Save the record."""
  96. db.session.add(self)
  97. if commit:
  98. try:
  99. db.session.commit()
  100. except Exception as e:
  101. current_app.logger.info(e)
  102. db.session.rollback()
  103. return self
  104. def delete(self, commit=True):
  105. """Remove the record from the database."""
  106. db.session.delete(self)
  107. if commit:
  108. try:
  109. db.session.commit()
  110. except:
  111. db.session.rollback()
  112. return self
  113. def to_dict(self, fields_list=None):
  114. """
  115. Args:
  116. fields (str list): 指定返回的字段
  117. """
  118. column_list = fields_list or [
  119. column.name for column in self.__table__.columns
  120. ]
  121. return {
  122. column_name: getattr(self, column_name)
  123. for column_name in column_list
  124. }
  125. @classmethod
  126. def create_or_update(cls, query_dict, update_dict=None):
  127. instance = db.session.query(cls).filter_by(**query_dict).first()
  128. if instance: # update
  129. if update_dict is not None:
  130. return instance.update(**update_dict)
  131. else:
  132. return instance
  133. else: # create new instance
  134. query_dict.update(update_dict or {})
  135. return cls.create(**query_dict)
  136. @classmethod
  137. def query_paginate(cls, page=1, limit=20, fields=None, order_by_list=[('id', 'desc')],
  138. filter_name_dict=None):
  139. """ 通用的分页查询函数
  140. Args:
  141. page (int): 页数
  142. limit (int): 每页个数
  143. order_by_list (tuple list): 用来指定排序的字段,可以传多个
  144. [ ('id', 1), ('name', -1) ],1表示正序,-1表示逆序
  145. or
  146. [ ('id', 'asc'), ('name', 'desc') ],1表示正序,-1表示逆序
  147. filter_name_dict (dict): 过滤条件,使用字典表示,使用字段名作为key,value
  148. 是{'operator': to_compare_value}, e.g.:
  149. {
  150. 'last_name': {'eq': 'wang'}, # 如果是dic使用key作为操作符
  151. 'age': {'>': 12}
  152. }
  153. Returns:
  154. if fields is not None:
  155. (keytuple_list, total_cnt) (tuple)
  156. else:
  157. (instance_list, total_cnt) (tuple)
  158. 前段查询参数规范:
  159. request.args 示例:
  160. ImmutableMultiDict([('limit', '10'), ('page', '1'), ('filter', '[{"field":"name","op":"eq","q":"g"},{
  161. "field":"id","op":"gt","q":"5"
  162. }]')])
  163. page: 页码
  164. limit: 每页限制
  165. order: 顺序,取值"asc" "desc". """'name', 'asc', 'model', 'desc'"""
  166. fields: 需要返回的字段
  167. filter: 过滤条件:
  168. {
  169. field: 需要过滤的字段
  170. op: 过滤操作符,支持"eq","neq","gt","gte","lt","lte","in","nin","like"
  171. q: 过滤值
  172. }
  173. """
  174. fields = (
  175. [getattr(cls, column) for column in fields] if fields is not None
  176. else None
  177. )
  178. if fields:
  179. query = db.session.query(*fields)
  180. else:
  181. query = db.session.query(cls)
  182. if order_by_list:
  183. for (field, order) in order_by_list:
  184. query = (
  185. query.order_by(getattr(cls, field)) if order == 1 else
  186. query.order_by(desc(getattr(cls, field)))
  187. )
  188. if filter_name_dict:
  189. p = parse_operator(cls, filter_name_dict)
  190. query = query.filter(*p)
  191. total_cnt = query.count()
  192. start = (page - 1) * limit
  193. query = query.offset(start).limit(limit)
  194. instance_or_keytuple_list = query.all()
  195. return instance_or_keytuple_list, total_cnt
  196. @classmethod
  197. def dump_schema(cls, items, fields, schema_class):
  198. """ 用来序列化从数据库查询出来的对象
  199. Args:
  200. items (instance list): obj list query from mysql
  201. fields (str list): fields need to dump
  202. schema_class (marshmallow.Schema): marshmallow.Schema class
  203. Returns:
  204. items, err
  205. """
  206. fields = (
  207. fields if fields else list(schema_class._declared_fields.keys())
  208. )
  209. schema = schema_class(many=True, only=fields)
  210. items, err = schema.dump(items)
  211. return items, err
  212. @classmethod
  213. def query_paginate_and_dump_schema(cls, page=1, limit=20, fields=None,
  214. order_by_list=None,
  215. filter_name_dict=None,
  216. schema_class=None):
  217. """ 分页查询并且返回dump后的对象,可以解决大部分查询问题 """
  218. assert schema_class
  219. items, total_cnt = cls.query_paginate(
  220. page, limit, fields, order_by_list, filter_name_dict
  221. )
  222. items, err = cls.dump_schema(items, fields, schema_class)
  223. return items, total_cnt
  224. def __repr__(self):
  225. return pformat(self.to_dict())
  226. @cached_property
  227. def column_name_set(self):
  228. return set([column.name for column in self.__table__.columns])
  229. @classmethod
  230. def get_common_fields(cls, fields=None):
  231. """ 防止传过来的fields含有该Model没有的字段 """
  232. if not fields:
  233. return []
  234. table_fields_set = set(
  235. [column.name for column in cls.__table__.columns]
  236. )
  237. return list(table_fields_set & set(fields))
  238. class Model(CRUDMixin, db.Model):
  239. """Base model class that includes CRUD convenience methods."""
  240. __abstract__ = True
  241. status_remove = -1
  242. status_default = 0
  243. status_available = 1
  244. # From Mike Bayer's "Building the app" talk
  245. # https://speakerdeck.com/zzzeek/building-the-app
  246. class SurrogatePK(object):
  247. """A mixin that adds a surrogate integer 'primary key' column named ``id`` to any declarative-mapped class."""
  248. __table_args__ = {'extend_existing': True}
  249. id = db.Column(db.Integer, primary_key=True)
  250. @classmethod
  251. def get_by_id(cls, record_id):
  252. """Get record by ID."""
  253. if any(
  254. (isinstance(record_id, basestring) and record_id.isdigit(),
  255. isinstance(record_id, (int))),
  256. ):
  257. return cls.query.get(int(record_id))
  258. return None
  259. def reference_col(tablename, nullable=False, pk_name='id', **kwargs):
  260. """Column that adds primary key foreign key reference.
  261. Usage: ::
  262. category_id = reference_col('category')
  263. category = relationship('Category', backref='categories')
  264. """
  265. return db.Column(
  266. db.ForeignKey('{0}.{1}'.format(tablename, pk_name)),
  267. nullable=nullable, **kwargs)