├── MANIFEST.in ├── .gitignore ├── thing ├── __init__.py └── thing.py ├── setup.py └── README.md /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.egg-info 3 | *.pyc 4 | build/* 5 | dist/* 6 | -------------------------------------------------------------------------------- /thing/__init__.py: -------------------------------------------------------------------------------- 1 | __name__ = 'thing' 2 | __version__ = '0.3.3' 3 | __author__ = 'lzyy' 4 | __license__ = 'BSD' 5 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | import os 3 | import sys 4 | import thing 5 | 6 | if sys.argv[-1] == 'publish': 7 | os.system('python setup.py sdist upload') 8 | sys.exit() 9 | 10 | setup( 11 | name = thing.__name__, 12 | version = thing.__version__, 13 | url = 'http://github.com/lzyy/thing', 14 | license = thing.__license__, 15 | author = thing.__author__, 16 | author_email = 'healdream@gmail.com', 17 | description = 'lightweight SQLAlchemy based ORM', 18 | long_description = open('README.md').read(), 19 | zip_safe = False, 20 | platforms = 'any', 21 | packages = ["thing"], 22 | include_package_data = True, 23 | install_requires = [ 24 | 'sqlalchemy', 25 | 'mysql-python', 26 | ], 27 | classifiers = [ 28 | 'Development Status :: 4 - Beta', 29 | 'Intended Audience :: Developers', 30 | 'License :: OSI Approved :: BSD License', 31 | 'Operating System :: OS Independent', 32 | 'Programming Language :: Python', 33 | 'Topic :: Database', 34 | 'Topic :: Software Development :: Libraries :: Python Modules' 35 | ], 36 | ) 37 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | /\ \ /\__\ ___ /\__\ /\ \ 3 | \:\ \ /:/ / /\ \ /::| | /::\ \ 4 | \:\ \ /:/__/ \:\ \ /:|:| | /:/\:\ \ 5 | /::\ \ /::\ \ ___ /::\__\ /:/|:| |__ /:/ \:\ \ 6 | /:/\:\__\ /:/\:\ /\__\ __/:/\/__/ /:/ |:| /\__\ /:/__/_\:\__\ 7 | /:/ \/__/ \/__\:\/:/ / /\/:/ / \/__|:|/:/ / \:\ /\ \/__/ 8 | /:/ / \::/ / \::/__/ |:/:/ / \:\ \:\__\ 9 | \/__/ /:/ / \:\__\ |::/ / \:\/:/ / 10 | /:/ / \/__/ /:/ / \::/ / 11 | \/__/ \/__/ \/__/ 12 | 13 |14 | 15 | # Thing 是什么? 16 | 17 | Thing是一个基于SQLAlchemy的配置简单、使用简单且灵活的ORM。 18 | 19 | # 使用方法 20 | 21 | 举个简单的例子,假如有3个表:comment, post, user, 3个表的字段分别是: 22 | 23 | comment表: 24 | ``` 25 | +---------+------------------+------+-----+---------+----------------+ 26 | | Field | Type | Null | Key | Default | Extra | 27 | +---------+------------------+------+-----+---------+----------------+ 28 | | id | int(11) unsigned | NO | PRI | NULL | auto_increment | 29 | | user_id | int(11) | YES | MUL | NULL | | 30 | | post_id | int(11) | YES | MUL | NULL | | 31 | | content | text | YES | | NULL | | 32 | +---------+------------------+------+-----+---------+----------------+ 33 | ``` 34 | 35 | post表: 36 | ``` 37 | +---------+------------------+------+-----+---------+----------------+ 38 | | Field | Type | Null | Key | Default | Extra | 39 | +---------+------------------+------+-----+---------+----------------+ 40 | | id | int(11) unsigned | NO | PRI | NULL | auto_increment | 41 | | user_id | int(11) | YES | MUL | NULL | | 42 | | created | int(11) | YES | | NULL | | 43 | | content | text | YES | | NULL | | 44 | | title | varchar(255) | YES | | NULL | | 45 | +---------+------------------+------+-----+---------+----------------+ 46 | ``` 47 | 48 | user表: 49 | ``` 50 | +-------+------------------+------+-----+---------+----------------+ 51 | | Field | Type | Null | Key | Default | Extra | 52 | +-------+------------------+------+-----+---------+----------------+ 53 | | id | int(11) unsigned | NO | PRI | NULL | auto_increment | 54 | | name | varchar(30) | YES | | NULL | | 55 | +-------+------------------+------+-----+---------+----------------+ 56 | ``` 57 | 58 | ## 定义Model 59 | 60 | 先来看看目录结构 61 | ``` 62 | ├── __init.py__ 63 | ├── conn.py # 用于数据库连接 64 | ├── models 65 | │ ├── __init__.py 66 | │ ├── comment.py 67 | │ ├── post.py 68 | │ ├── user.py 69 | └── test.py 70 | ``` 71 | test.py就是进行测试的地方,先来看看各个model的内容: 72 | 73 | ### comment.py 74 | 75 | ``` 76 | from thing import thing 77 | 78 | class Comment(thing.Thing): 79 | _belongs_to = { 80 | 'post': { 81 | 'model': 'models.post.Post', 82 | 'foreign_key': 'post_id', 83 | }, 84 | 'author': { 85 | 'model': 'models.user.User', 86 | 'foreign_key': 'user_id', 87 | }, 88 | } 89 | ``` 90 | 91 | ### post.py 92 | 93 | ``` 94 | from thing import thing 95 | 96 | class Post(thing.Thing): 97 | _belongs_to = { 98 | 'author': { 99 | 'model': 'models.user.User', 100 | 'foreign_key': 'user_id', 101 | } 102 | } 103 | _has_many = { 104 | 'comments': { 105 | 'model': 'models.comment.Comment', 106 | 'foreign_key': 'user_id', 107 | } 108 | } 109 | ``` 110 | 111 | ### user.py 112 | 113 | ``` 114 | from thing import thing 115 | 116 | class User(thing.Thing): 117 | _has_many = { 118 | 'posts': { 119 | 'model': 'models.post.Post', 120 | 'foreign_key': 'user_id' 121 | }, 122 | 'comments': { 123 | 'model': 'models.comment.Comment', 124 | 'foreign_key': 'user_id' 125 | } 126 | } 127 | ``` 128 | 129 | 再来看看conn.py 130 | 131 | ### conn.py 132 | 133 | ``` 134 | from thing import thing 135 | 136 | config = { 137 | 'db': { 138 | 'master': { 139 | 'url': 'mysql://root:123456@127.0.0.1:3306/test?charset=utf8', 140 | 'echo': False, 141 | }, 142 | 'slave': { 143 | 'url': 'mysql://root:123456@127.0.0.1:3306/test?charset=utf8', 144 | 'echo': False, 145 | }, 146 | }, 147 | 'redis': { 148 | 'host': 'localhost', 149 | 'port': 6379, 150 | 'db': 1, 151 | }, 152 | 'thing': { 153 | 'debug': True, 154 | } 155 | } 156 | 157 | thing.Thing.config(config) 158 | ``` 159 | 160 | OK,万事具备,开工! 161 | 162 | ``` 163 | import conn 164 | from models.comment import Comment 165 | from models.user import User 166 | from models.post import Post 167 | 168 | # -------- 插入数据 -------- 169 | user = User() 170 | user.name = 'foo' 171 | user.save() 172 | # 或者 user = User(name='foo').save() 173 | 174 | # -------- 获取数据 -------- 175 | user = User().find(1) 176 | print user.name 177 | 178 | # -------- 获取关联数据 ------- 179 | posts = User().find(1).posts.findall() 180 | # 如果要设置offset / limit, 在findall里加入参数即可 181 | # posts = User().find(1).posts.findall(offset = 0, limit = 20) 182 | 183 | # ------- 删除数据 ------- 184 | User().find(1).delete() 185 | 186 | # ------- 更新数据 ------- 187 | user = User().find(1) 188 | user.name = 'bar' 189 | user.save() 190 | ``` 191 | 192 | # 动态查询 193 | 194 | 这个是受Rails影响,觉得很方便就拿来了。比如 `Post().count_by_user_id(3)`,就可以找到user_id为3的用户发表的文章数量。要获取`user_id`为3的用户发表的文章,可以`Post().findall_by_user_id(3, limit=20)`,比起`Post().where('user_id', '=', 3).findall()`更加简洁和明了。 195 | 196 | # 关于性能和缓存 197 | 198 | Thing内置了Redis作为缓存,你甚至都不需要知道Redis的存在,正常该怎么用还怎么用,Thing会自动处理缓存的生成、读取、过期、删除等操作。 199 | 200 | 假设表post里有5条数据,在获取每条post后,还想获取该post对应的用户信息,代码如下: 201 | 202 | ``` 203 | posts = Post().findall(limit=5) 204 | 205 | for post in posts: 206 | print post.author 207 | ``` 208 | 209 | 在开启Debug的情况下,可以在终端看到如下显示: 210 | 211 | ``` 212 | DEBUG - [cost:0.0032] - SELECT post.id, post.user_id, post.created, post.content, post.title 213 | FROM post ORDER BY post.id DESC 214 | LIMIT :param_1 OFFSET :param_2 215 | DEBUG - Cache Read: thing.User:1 216 | {u'id': 1, u'name': u'lzyy'} 217 | DEBUG - Cache Read: thing.User:1 218 | {u'id': 1, u'name': u'lzyy'} 219 | DEBUG - Cache Read: thing.User:1 220 | {u'id': 1, u'name': u'lzyy'} 221 | DEBUG - Cache Read: thing.User:1 222 | {u'id': 1, u'name': u'lzyy'} 223 | DEBUG - Cache Read: thing.User:1 224 | {u'id': 1, u'name': u'lzyy'} 225 | ``` 226 | 227 | 可以看到用户的信息都是从缓存中读取的,所以不用担心n+1的问题。 228 | 假如用户的信息被更新,缓存也会自动更新。 229 | 230 | # 其他 231 | 232 | * 配置信息里的`master`和`slave`为必选项,可以相同。Thing会根据不同的查询,自动找到对应的db。如find/findall会找slave,update/delete会找master。 233 | * 配置信息里的redis项为必选项。 234 | * 动态查询目前支持`find_by`, `findall_by`, `findall_in`, `count_by` 235 | * 内置了8个钩子,会在相应的事件发生时被调用,分别是:`_before_insert`,`_after_insert`,`_before_update`,`_after_update`,`_before_delete`,`_after_delete`,`_before_find`,`_after_find`,可以在子类里覆盖这些方法来实现自己的逻辑。 236 | * 复杂的SQL可以使用`execute`方法,返回的结果是SQLAlchemy的ResultProxy 237 | * 如果要一次更新多处的话,可以使用`updateall`方法,`Post().where('user_id', '=', 1).updateall(user_id=2)` 238 | * 表名如果和小写的类名不一样的话,可以在子类里重新设置`_tablename` 239 | * 每个表一定要有主键,默认为`id`,可以在子类里重新设置`_primary_key` 240 | * 支持has_many和belongs_to,可以在子类里定义`_has_many`和`_belongs_to` 241 | * 没有`join`方法 242 | 243 | # ChangeLog 244 | 245 | ## 0.3.3 246 | * 修复无法从pip安装的bug 247 | * 修复安装时对redis-py的依赖 248 | * `import thing`变为`from thing import thing` 249 | 250 | ## 0.3.2 251 | * 修复了并发情况下会出现「Exception _mysql_exceptions.ProgrammingError: (2014, "Commands out of sync; you can't run this command now"」错误。 252 | * Redis缓存变为可配置项。如果不想要Redis的话,在config里取消`Redis`配置即可。 253 | 254 | ## 0.3.1 255 | * 取消了对Validation的支持 256 | * 取消了对Sharding和Partition的支持 257 | * 取消了事件分发机制 258 | -------------------------------------------------------------------------------- /thing/thing.py: -------------------------------------------------------------------------------- 1 | #coding=utf-8 2 | import time 3 | import logging 4 | import json 5 | import sys 6 | try: 7 | import redis 8 | except ImportError as e: 9 | pass 10 | from sqlalchemy import Table, MetaData, create_engine 11 | from sqlalchemy.sql import select, func, and_ 12 | from sqlalchemy.sql.expression import label 13 | from functools import partial 14 | 15 | class AttributeDict(dict): 16 | __getattr__ = dict.__getitem__ 17 | __setattr__ = dict.__setitem__ 18 | 19 | class ThingException(Exception): 20 | pass 21 | 22 | class Thing(object): 23 | 24 | # change this if your pk is not id 25 | # current doesn't support multi pk 26 | _primary_key = 'id' 27 | 28 | # if leave it as None, lower classname will be used 29 | _tablename = None 30 | 31 | # record tables schema infomation 32 | _table_schemas = {} 33 | 34 | # _has_many = {'posts': {'model': post.Post(), 'foreign_key': 'user_id'}} 35 | # then u can use like this: current_user.posts.findall() 36 | _has_many = {} 37 | 38 | # _belongs_to = {'post': {'model': post.Post(), 'foreign_key': 'post_id'}} 39 | # then u can use like this: comment.post.title 40 | _belongs_to = {} 41 | 42 | # when a list of rows are ready to be delete, put it here 43 | # so cache can clear these records 44 | __tobe_deleted_rows = [] 45 | 46 | __tobe_updated_rows = [] 47 | 48 | __logger = None 49 | 50 | @staticmethod 51 | def config(config): 52 | """ 53 | config is like this: 54 | 55 | config = { 56 | 'db': { 57 | 'master': { 58 | 'url': 'mysql://username:password@127.0.0.1:3306/dbname?charset=utf8', 59 | 'echo': False, 60 | }, 61 | 'slave': { 62 | 'url': 'mysql://username:password@127.0.0.1:3306/dbname?charset=utf8', 63 | 'echo': False, 64 | }, 65 | }, 66 | 'redis': { 67 | 'host': 'localhost', 68 | 'port': 6379, 69 | 'db': 0, 70 | } 71 | 'thing': 72 | 'debug': True, 73 | } 74 | 75 | there must have at least master and slave section in db section 76 | """ 77 | Thing._config = config 78 | Thing._db_conn = {} 79 | if Thing._config.get('redis'): 80 | Thing._redis_conn = redis.StrictRedis(host=config['redis']['host'], port=config['redis']['port'], db=config['redis']['db']) 81 | 82 | def debug(self, message): 83 | if Thing._config['thing'].get('debug'): 84 | if not Thing.__logger: 85 | Thing.__logger = logging.getLogger(__name__) 86 | Thing.__logger.setLevel(logging.DEBUG) 87 | formatter = logging.Formatter('DEBUG - %(message)s') 88 | handler_stream = logging.StreamHandler(sys.stdout) 89 | handler_stream.setFormatter(formatter) 90 | handler_stream.setLevel(logging.DEBUG) 91 | Thing.__logger.addHandler(handler_stream) 92 | Thing.__logger.debug(message) 93 | 94 | def compile_query(self, query): 95 | # TODO format query with values 96 | return query 97 | 98 | @staticmethod 99 | def _get_conn(table_name, is_read): 100 | """ 101 | if this is read operation and table_name.slave exists in config['db'], then this section is used 102 | else slave section will be used 103 | 104 | if this is write operation and table_name.master exists in config['db'], then this section is used 105 | else master section will be used 106 | """ 107 | section = '%s.%s' % (table_name, 'slave' if is_read else 'master') 108 | if not section in Thing._config['db']: 109 | # make sure there is 'slave' and 'master' section in config['db'] 110 | section = 'slave' if is_read else 'master' 111 | 112 | # do not connect multi times 113 | conn = Thing._db_conn.setdefault(section, None) 114 | if not Thing._db_conn.get(section): 115 | url = Thing._config['db'][section]['url'] 116 | kwargs = {k:v for k, v in Thing._config['db'][section].items() if k != 'url'} 117 | Thing._db_conn[section]= create_engine(url, **kwargs) 118 | 119 | conn = Thing._db_conn[section].connect().execution_options(autocommit=True) 120 | return conn 121 | 122 | def __init__(self, **fields): 123 | """ 124 | set fields' init value is allowed 125 | """ 126 | self._init_env() 127 | 128 | for field_name, field_value in fields.items(): 129 | self._unsaved_items[field_name] = field_value 130 | 131 | def _init_env(self): 132 | self._unsaved_items = {} 133 | self._current_item = {} 134 | self._filters = [] 135 | self._results = [] 136 | self._current_index = -1 137 | self._find_fields = [] 138 | self._findall_fields = [] 139 | self._count_by_fields = [] 140 | self._findall_in_field = None 141 | self._tablename = self._tablename or self.__class__.__name__.lower() 142 | self._selected_fields = [self.table] 143 | self._order_by = getattr(self.table.c, self._primary_key).desc() 144 | 145 | @property 146 | def saved(self): 147 | return not bool(self._unsaved_items) 148 | 149 | def execute(self, query_str, is_read = True): 150 | """ 151 | execute raw sql 152 | """ 153 | conn = Thing._get_conn(self._tablename, is_read) 154 | result = conn.execute(query_str) 155 | conn.close() 156 | return result 157 | 158 | def __delattr__(self, key): 159 | if key in self._current_item: 160 | del self._current_item[key] 161 | elif key in self._unsaved_items: 162 | del self._unsaved_items[key] 163 | 164 | def __getattr__(self, key): 165 | 166 | def _import(name): 167 | mod = __import__(name) 168 | components = name.split('.') 169 | for comp in components[1:]: 170 | mod = getattr(mod, comp) 171 | return mod 172 | 173 | if key in self._unsaved_items: 174 | return self._unsaved_items[key] 175 | elif key in self._current_item: 176 | # value = getattr(self._current_item, key) 177 | value = self._current_item[key] 178 | return '' if value is None else value 179 | elif key[:8] == 'find_by_': 180 | if key.find('_and_') == -1: 181 | self._find_fields.append(key[8:]) 182 | else: 183 | self._find_fields = key[8:].split('_and_') 184 | return self 185 | elif key[:11] == 'findall_by_': 186 | if key.find('_and_') == -1: 187 | self._findall_fields.append(key[11:]) 188 | else: 189 | self._findall_fields = key[11:].split('_and_') 190 | return self 191 | elif key[:11] == 'findall_in_': 192 | self._findall_in_field = key[11:] 193 | return self 194 | elif key[:9] == 'count_by_': 195 | if key.find('_and_') == -1: 196 | self._count_by_fields.append(key[9:]) 197 | else: 198 | self._count_by_fields = key[9:].split('_and_') 199 | return self 200 | elif key in self._has_many: 201 | model_name = self._has_many[key]['model'] 202 | if model_name.find('.') != -1: 203 | sections = model_name.split('.') 204 | # __import__ only import first section 205 | model = getattr(_import('.'.join(sections[:-1])), sections[-1])() 206 | else: 207 | model = locals()[model_name]() 208 | model.where(self._has_many[key]['foreign_key'], '=', getattr(self, self._primary_key)) 209 | return model 210 | elif key in self._belongs_to: 211 | model_name = self._belongs_to[key]['model'] 212 | if model_name.find('.') != -1: 213 | sections = model_name.split('.') 214 | __import__('.'.join(sections[:-1])) 215 | model = getattr(sys.modules['.'.join(sections[:-1])], sections[-1])() 216 | else: 217 | model = locals()[model_name]() 218 | model.find(getattr(self, self._belongs_to[key]['foreign_key'])) 219 | return model 220 | 221 | raise ThingException('key:{key} not found'.format(key = key)) 222 | 223 | def __call__(self, *args, **kwargs): 224 | if self._find_fields: 225 | for i, val in enumerate(self._find_fields): 226 | self.where(val, '=', args[i]) 227 | self._find_fields = [] 228 | result = self.find() 229 | return result 230 | if self._findall_fields: 231 | for i, val in enumerate(self._findall_fields): 232 | self.where(val, '=', args[i]) 233 | self._findall_fields = [] 234 | result = self.findall(**kwargs) 235 | return result 236 | if self._count_by_fields: 237 | for i, val in enumerate(self._count_by_fields): 238 | self.where(val, '=', args[i]) 239 | self._count_by_fields = [] 240 | result = self.count() 241 | return result 242 | if self._findall_in_field: 243 | self.where(self._findall_in_field, 'in', args[0]) 244 | self._findall_in_field = None 245 | result = self.findall() 246 | return result 247 | return self 248 | 249 | def __setattr__(self, key, val): 250 | if key[0] != '_': 251 | self._unsaved_items[key] = val 252 | else: 253 | object.__setattr__(self, key, val) 254 | 255 | def __len__(self): 256 | if self._results: 257 | return len(self._results) 258 | elif self._current_item: 259 | return 1 260 | return 0 261 | 262 | def _before_insert(self): 263 | pass 264 | 265 | def _before_update(self): 266 | pass 267 | 268 | def _after_insert(self): 269 | if Thing._config.get('redis'): 270 | Thing._redis_conn.set('thing.%s:%s' % (self.__class__.__name__, self._current_item[self._primary_key]), 271 | json.dumps(self.to_dict())) 272 | 273 | def _after_update(self): 274 | if Thing._config.get('redis'): 275 | if self.__tobe_updated_rows: 276 | for row in self.__tobe_updated_rows: 277 | Thing._redis_conn.delete('thing.%s:%s' % (self.__class__.__name__, row[self._primary_key])) 278 | elif self._current_item: 279 | self._after_insert() 280 | 281 | def _before_delete(self): 282 | if Thing._config.get('redis'): 283 | if self._primary_key in self._current_item.keys(): 284 | Thing._redis_conn.delete('thing.%s:%s' % (self.__class__.__name__, self._current_item[self._primary_key])) 285 | elif self.__tobe_deleted_rows: 286 | for row in self.__tobe_deleted_rows: 287 | Thing._redis_conn.delete('thing.%s:%s' % (self.__class__.__name__, row[self._primary_key])) 288 | 289 | def _after_delete(self): 290 | pass 291 | 292 | def _before_find(self, val): 293 | if Thing._config.get('redis'): 294 | key_name = 'thing.%s:%s' % (self.__class__.__name__, val) 295 | result = Thing._redis_conn.get(key_name) 296 | if result: 297 | result = json.loads(result) 298 | self.debug('Cache Read: %s' % key_name) 299 | return result 300 | 301 | def _after_find(self, val): 302 | if not val: 303 | if not self._current_item: 304 | return 305 | else: 306 | val = getattr(self,_current_item, self._primary_key) 307 | 308 | if Thing._config.get('redis'): 309 | key_name = 'thing.%s:%s' % (self.__class__.__name__, val) 310 | result = Thing._redis_conn.get(key_name) 311 | if not result: 312 | Thing._redis_conn.set(key_name, json.dumps(self.to_dict())) 313 | 314 | def _before_findall(self): 315 | pass 316 | 317 | def save(self): 318 | conn = Thing._get_conn(self._tablename, False) 319 | 320 | # fill the _unsaved_items with _current_item if not empty 321 | if self._current_item: 322 | for key, val in self._current_item.items(): 323 | if not key in self._unsaved_items: 324 | self._unsaved_items[key] = val 325 | 326 | if self._primary_key in self._unsaved_items.keys(): 327 | primary_key_val = self._unsaved_items.pop(self._primary_key) 328 | query = (self.table.update() 329 | .where(getattr(self.table.c, self._primary_key) == primary_key_val) 330 | .values(**self._unsaved_items)) 331 | self._before_update() 332 | start_time = time.time() 333 | conn.execute(query) 334 | self.debug('[cost:%.4f] - %s' % (time.time() - start_time, query)) 335 | 336 | query = self.table.select().where(getattr(self.table.c, self._primary_key) == primary_key_val) 337 | self._current_item = conn.execute(query).first() 338 | self._after_update() 339 | else: 340 | self._before_insert() 341 | query = self.table.insert().values(**self._unsaved_items) 342 | primary_key_val = conn.execute(query).inserted_primary_key[0] 343 | 344 | query = self.table.select().where(getattr(self.table.c, self._primary_key) == primary_key_val) 345 | start_time = time.time() 346 | self._current_item = conn.execute(query).first() 347 | self.debug('[cost:%.4f] - %s' % (time.time() - start_time, query)) 348 | self._after_insert() 349 | 350 | self._unsaved_items = {} 351 | conn.close() 352 | return primary_key_val 353 | 354 | def delete(self): 355 | conn = Thing._get_conn(self._tablename, False) 356 | 357 | if self._primary_key in self._current_item.keys(): 358 | self._before_delete() 359 | pk = self._primary_key 360 | query = self.table.delete().where(getattr(self.table.c, pk) == self._current_item[pk]) 361 | start_time = time.time() 362 | rowcount = conn.execute(query).rowcount 363 | self.debug('[cost:%.4f] - %s' % (time.time() - start_time, query)) 364 | else: 365 | self.__tobe_deleted_rows = self.table.select([self._primary_key]).query(and_(*self._filters)).findall() 366 | self._before_delete() 367 | query = self.table.delete(and_(*self._filters)) 368 | self.__tobe_deleted_rows = [] 369 | start_time = time.time() 370 | rowcount = conn.execute(query).rowcount 371 | self.debug('[cost:%.4f] - %s' % (time.time() - start_time, query)) 372 | 373 | self._after_delete() 374 | conn.close() 375 | return rowcount 376 | 377 | @property 378 | def table(self): 379 | """ 380 | get current table info 381 | """ 382 | if Thing._table_schemas.get(self._tablename, None) is None: 383 | conn = Thing._get_conn(self._tablename, True) 384 | Thing._table_schemas[self._tablename] = Table(self._tablename, MetaData(), autoload = True, autoload_with = conn) 385 | return Thing._table_schemas[self._tablename] 386 | 387 | def where(self, field, operation, val): 388 | # check if field has function in it 389 | field_obj = None 390 | if field.find('(') != -1: 391 | sql_func = getattr(func, field[:field.find('(')]) 392 | field = field[field.find('(')+1: -1] 393 | field_obj = sql_func(getattr(self.table.c, field)) 394 | else: 395 | field_obj = getattr(self.table.c, field) 396 | 397 | op_dict = {'=': '__eq__', 398 | '>': '__gt__', 399 | '>=': '__ge__', 400 | '<': '__lt__', 401 | '<=': '__le__', 402 | '!=': '__ne__', 403 | 'in': 'in_', 404 | } 405 | 406 | for op, op_method in op_dict.items(): 407 | if op == operation: 408 | operation = op_method 409 | break 410 | 411 | self._filters.append(getattr(field_obj, operation)(val)) 412 | return self 413 | 414 | def order_by(self, order_by): 415 | """ 416 | order_by (string): if start with '-' means desc 417 | """ 418 | if order_by[0] == '-': 419 | self._order_by = getattr(self.table.c, order_by[1:]).desc() 420 | else: 421 | self._order_by = getattr(self.table.c, order_by) 422 | return self 423 | 424 | def select(self, fields): 425 | self._selected_fields = [] 426 | for field in fields: 427 | field_obj = None 428 | if field.find('(') != -1: 429 | sql_func = getattr(func, field[:field.find('(')]) 430 | if field.find(' as ') == -1: 431 | field = field[field.find('(')+1: -1] 432 | field_obj = sql_func(getattr(self.table.c, field)) 433 | else: 434 | field, as_label = field.split(' as ') 435 | field = field[field.find('(')+1: -1] 436 | field_obj = sql_func(getattr(self.table.c, field)).label(as_label) 437 | else: 438 | field_obj = getattr(self.table.c, field) 439 | self._selected_fields.append(field_obj) 440 | return self 441 | 442 | def find(self, val = None): 443 | conn = Thing._get_conn(self._tablename, True) 444 | if val: 445 | result = self._before_find(val) 446 | if result: 447 | self._current_item = result 448 | return self 449 | query = self.table.select().where(getattr(self.table.c, self._primary_key) == val) 450 | else: 451 | query = select(self._selected_fields, and_(*self._filters)) 452 | 453 | start_time = time.time() 454 | result = conn.execute(query).first() 455 | self.debug('[cost:%.4f] - %s' % (time.time() - start_time, query)) 456 | 457 | self._after_find(val) 458 | 459 | self._current_item = {} if not result else result 460 | # empty current filter 461 | self._filters = [] 462 | self._selected_fields = [self.table] 463 | conn.close() 464 | return self 465 | 466 | def findall(self, limit = -1, offset = 0): 467 | conn = Thing._get_conn(self._tablename, True) 468 | 469 | query = partial(select, self._selected_fields) 470 | query = query(and_(*self._filters)) if self._filters else query() 471 | 472 | if limit == -1: 473 | query = query.order_by(self._order_by).offset(offset) 474 | else: 475 | query = query.order_by(self._order_by).limit(limit).offset(offset) 476 | 477 | result = self._before_findall() 478 | if result: 479 | self._results = result 480 | return self 481 | 482 | start_time = time.time() 483 | self._results = conn.execute(query).fetchall() 484 | self.debug('[cost:%.4f] - %s' % (time.time() - start_time, query)) 485 | 486 | # empty current filter 487 | self._filters = [] 488 | self._selected_fields = [self.table] 489 | conn.close() 490 | return self 491 | 492 | def updateall(self, **fields): 493 | conn = Thing._get_conn(self._tablename, False) 494 | 495 | _query = partial(select, [self._primary_key]) 496 | _query = _query(and_(*self._filters)) if self._filters else _query() 497 | self.__tobe_updated_rows = conn.execute(_query).fetchall() 498 | self._after_update() 499 | self.__tobe_updated_rows = [] 500 | 501 | update = self.table.update() 502 | if self._filters: 503 | for _filter in self._filters: 504 | update = update.where(_filter) 505 | query = update.values(**fields) 506 | 507 | 508 | start_time = time.time() 509 | rowcount = conn.execute(query).rowcount 510 | self.debug('[cost:%.4f] - %s' % (time.time() - start_time, query)) 511 | conn.close() 512 | 513 | return rowcount 514 | 515 | def get_field(self, field): 516 | """ 517 | after findall(), you can call get_field to fetch certain field into a list 518 | """ 519 | field_content = [] 520 | for result in self._results: 521 | field_content.append(getattr(result, field)) 522 | return field_content 523 | 524 | def to_dict(self): 525 | """ 526 | make current find() result into dict 527 | """ 528 | d = {} 529 | for column_name in self.table.columns.keys(): 530 | if hasattr(self._current_item, column_name): 531 | d[column_name] = getattr(self._current_item, column_name) 532 | return AttributeDict(d) 533 | 534 | def to_list(self): 535 | """ 536 | make current findall() result into list 537 | """ 538 | results = [] 539 | for result in self._results: 540 | results.append(result) 541 | return results 542 | 543 | def __repr__(self): 544 | if self._current_item: 545 | return repr(self._current_item) 546 | if self._results: 547 | return repr(self._results) 548 | return '<%s.%s object at %s>' % ( 549 | self.__class__.__module__, 550 | self.__class__.__name__, 551 | hex(id(self)) 552 | ) 553 | 554 | def count(self): 555 | """ 556 | get current query's count 557 | """ 558 | conn = Thing._get_conn(self._tablename, True) 559 | query = select([func.count(getattr(self.table.c, self._primary_key))], and_(*self._filters)) 560 | start_time = time.time() 561 | result = conn.execute(query).scalar() 562 | self.debug('[cost:%.4f] - %s' % (time.time() - start_time, query)) 563 | conn.close() 564 | return result 565 | 566 | def reset(self): 567 | self._init_env() 568 | return self 569 | 570 | def __iter__(self): 571 | return self 572 | 573 | def next(self): 574 | if self._current_index < len(self._results) - 1: 575 | self._current_index += 1 576 | self._current_item = self._results[self._current_index] 577 | return self 578 | else: 579 | raise StopIteration 580 | --------------------------------------------------------------------------------