ORM基本使用¶
使用要求¶
需要安装sqlalchemy 0.7+以上的版本。如果你使用sqlite,则python 2.6+就自带了。如果 使用其它的数据库,则还需要安装相应的包。sqlalchemy本身是不带的。
配置¶
首先将 uliweb.contrib.orm
添加到 apps/settings.ini
的 INSTALLED_APPS
中去。 uliweb.contrib.orm
的settings.ini中已经提供了几个缺省的配置项,用来控制ORM 的行为:
[ORM]
DEBUG_LOG = False
AUTO_CREATE = True
AUTO_TRANSACTION_IN_NOTWEB = False
AUTO_TRANSACTION_IN_WEB = False
CHECK_MAX_LENGTH = False
CONNECTION = 'sqlite:///database.db'
CONNECTION_ARGS = {}
CONNECTION_TYPE = 'long'
STRATEGY = 'threadlocal'
PK_TYPE = 'int'
CONNECTIONS = {}
TABLENAME_CONVERTER = None
NULLABLE = True
SERVER_DEFAULT = False
MANYTOMANY_INDEX_REVERSE = False
PATCH_NONE = 'empty'
[MIDDLEWARES]
你可以在apps/settings.ini中覆盖它们。
下面对这些配置项分别解释一下:
DEBUG_LOG¶
用来切换是否显示SQLAlchemy的日志。如果设置为 True
,则SQL语句会输出 到日志中。缺省为 False
。
AUTO_CREATE¶
用于切换是否可以自动建表。在缺省情况下为 False
,但是在测试代码中 可以考虑置为 True
以便可以自动建表。当它设为 False
时,有几种创建数据库表的方法:
- 通过
uliweb syncdb
或uliweb reset
等命令来自动建表。不过syncdb
在执行时,只会对数据库不存在的表进行创建处理,对于已经存在,但是表结构变化的表,不会进行更新处理。而reset
则是先将表drop
掉,再创建,所以总是新的。但是它同时也会将表删除。 - 通过
uliweb alembic
命令集来处理。它需要安装uliweb-alembic
包。它可以实现表的创建,表结构的修改等处理,所以建议使用这种方式来处理。 - 手工创建 ,不过手工创建比较麻烦不建议这样的。如果要做,可以先通过
uliweb sqltable tablename
命令得到对应的建表SQL语句,然后再手工创建。
为什么不建议在正式运行时打开自动建表。因为:自动建表每次都要检查数据表是否存在,存在性能问题。同时对于sqlite数据库有一个问题:如果在你执行一个事务时,非查询和更新类的语句 会引发事务的自动提交。而自动建表就是会先查找表是否存在,因此会破坏事务的处理。
AUTO_TRANSACTION_IN_NOTWEB (0.3修改)¶
用于指示是否在非WEB环境下,当执行 do_
时自动启动事务,详情参见下面关于多数据库连接的说明。非WEB环境主要是指守护和批处理,因此建议手工来处理事务。
AUTO_TRANSACTION_IN_WEB (0.3修改)¶
用于指示是否在WEB环境下自动启动事务。在添加 uliweb.contrib.orm
时,会自动安装 transcation_middleware 中间件,因此在缺省情况下,它会通过中间件的方式来自动创建和处理事务。
CHECK_MAX_LENGTH¶
是否当字段为 Varchar 或 Char 时检查max_length参数是否设置。这两种类型分别对应 Uliorm 中的 StringProperty 和 CharProperty,缺省情况下,max_length=255,因此 max_length 其实可以不用定义。但是为了让程序更清晰,可以把这个配置设为 True
,这样在定义这两种字段时,max_length 就一定要给出,否则报错。
CONNECTION¶
用于设置数据库连接串。它是遵循SQLAlchemy的要求的。(
普通的格式为:
driver://username:password@host:port/database
示例如下:
#sqlite
sqlite_db = create_engine('sqlite:////absolute/path/to/database.txt')
sqlite_db = create_engine('sqlite:///d:/absolute/path/to/database.txt')
sqlite_db = create_engine('sqlite:///relative/path/to/database.txt')
sqlite_db = create_engine('sqlite://') # in-memory database
sqlite_db = create_engine('sqlite://:memory:') # the same
# postgresql
pg_db = create_engine('postgres://scott:tiger@localhost/mydatabase')
# mysql
mysql_db = create_engine('mysql://scott:tiger@localhost/mydatabase')
# pymysql
mysql_db = create_engine('mysql+pymysql://scott:tiger@localhost/mydatabase')
# oracle
oracle_db = create_engine('oracle://scott:tiger@127.0.0.1:1521/sidname')
# oracle via TNS name
oracle_db = create_engine('oracle://scott:tiger@tnsname')
# mssql using ODBC datasource names. PyODBC is the default driver.
mssql_db = create_engine('mssql://mydsn')
mssql_db = create_engine('mssql://scott:tiger@mydsn')
# firebird
firebird_db = create_engine('firebird://scott:tiger@localhost/sometest.gdm')
CONNECTION_TYPE¶
用于指明连接模式: 'long'
为长连接,会在启动时建立。 'short'
为短连接,只会在每个请求时建立。缺省值为 'long'
即长连接。
CONNECTION_ARGS¶
用于除连接串之外的一些参数。SQLAlchemy中,创建引擎时要使用:
create_engine(connection, **kwargs)
而CONNECTION_ARGS将传入到kwargs中。在某些connection中其实还可以带一些类QUREY_STRING 的东西,如在对mysql的连接中,可以在连接串后面添加 '?charset=utf8
` 。而这个参 数是会直接传给更底层的mysql的驱动。而CONNECTION_ARGS是传给create_engine的,所以 还是有所不同。
STRATEGY¶
连接策略。此为sqlalchemy的连接参数,可以选择的值为 plain
和 threadlocal
. 其中 threadlocal
是为了实现无连接的执行。在sqlalchemy中,一般我们在执行sql命令 时或者使用engine或connection来执行。这样有时会感觉比较麻烦。于是如果在创建连接 时使用 strategy='threadlocal'
来创建,那么会在线程范围内创建一个共享的连接, 这样在执行sql时只要如:
select().execute()
就可以了。这就是无连接的执行方式。不过这样的方式在我的使用过程中感觉也有一点问题. 主要就是连接池的问题。uliweb在缺省情况下会采用长连接的策略。于是在执行完一个请求 时会close掉连接,这样可以把连接释放回去。但是发现 threadlocal 方式释放有问题,因为 它是共享的,其实无法真正的释放。所以uliweb在每个请求进来时会主动创建连接,然后在 返回时进行释放。它使用的并不是共享方式的连接。那么共享方式的连接主要是在命令行 或批处理执行时使用比较方便。在View处理中,建议都使用 do_
来进行包装。
PK_TYPE¶
表示主键ID使用的类型。缺省情况下,ID类型为 Integer, 但是如果数据特别多,则有可能超过Integer的最大值,所以可以通过它改为 bigint
,这样范围就更大了。
CONNECTIONS¶
数据库多连接设置(详情可以参见 多数据库连接 的文档)。uliweb是支持多个数据库连接,自然也支持多个数据库。 为了保持和以前使用方式的兼容。在 CONNECTIONS
中一般只要设置非缺省的数据库, 而缺省的数据库仍然使用原来的处理方式。 CONNECTIONS
的设置格式为:
CONNECTIONS = {
'test': {
'CONNECTION':'mysql://root:limodou@localhost/test2?charset=utf8',
'CONNECTION_TYPE':'short',
}
}
上面代码设置了一个名为 test
的连接。 CONNECTIONS
本身是一个dict,可以 设置多个连接。每个连接可以使用的参数为:
DEBUG_LOG = False
CONNECTION =
CONNECTION_TYPE = 'long'
CONNECTION_ARGS = {}
STRATEGY = 'plain'
TABLENAME_CONVERTER¶
用来设置文件名转换规则,缺省为 None
表示Model类名的小写作为表名。如果你还有其它的要求,可以定义一个函数,然后配置到这个参数中,如:
def tablename_converter(name):
return 't_%s' % name
然后配置到settings.ini中:
TABLENAME_CONVERTER = 'path.to.tablename_converter'
NULLABLE¶
字段是否可以为 NULL 的全局设置。缺省为 True
表示在定义字段时可以,数据库的值可以为 NULL。你可以通过修改为 False
来强制要求所有字段不能为 NULL,也可以针对单个字段传入 nullable=True
来设置允许为 NULL。
SERVER_DEFAULT¶
建表时,字段的缺省值设置。缺省为 False
表示没有指定缺省字段。它是全局设置,当置为 True
,每种类型都有自已的缺省值。也可以在定义某个字段时,通过传入 server_default
来设置数据库字段的缺省值。关于更详细的说明,参见字段定义时的常见参数说明关于 server_default
的说明。
MANYTOMANY_INDEX_REVERSE¶
对于 ManyToMany 字段是否要建立反向关系的索引。
例如 A 和 B 两个 Model,在A中定义了一个 bs 的ManyToMany的字段,这样在表中会建立 (a_id, b_id)
的一个索引。如果这时 MANYTOMANY_INDEX_REVERSE
为 False
, 则并不会创建一个 (b_id, a_id)
的索引。如果置为 True
则会创建。如果不想这个功能全局生效,还可以在定义 ManyToMany
时传入 index_reverse=True
的参数。
PATCH_NONE (0.3修改)¶
当你使用 0.9 版本的 SQLAlchemy 时,对于 None 在条件中的处理会发生变化。在 0.8 版本中, 这样的代码 None 是会被忽略掉:
condition = None
if xxx:
condition = (Model.c.id == n) & None
但是在 0.9 中,None会转为 NULL
从而造成上面的代码为 False,所以要么你将上面的代码改为:
from sqlalchemy.sql import true
condition = true()
if xxx:
condition = (Model.c.id == n) & None
如果 PATCH_NONE 为 'empty'
时,会保证 0.9 对 None 的处理和 0.8一样。也可以设置它的值为 'exception'
这样,在 0.9 版本中,当条件与 None 进行与操作时,会抛出异常。
MIDDLEWARES¶
安装 uliweb.contrib.orm app会自动添加 TransactionMiddle ,这样将自动启动事务。
Model 定义¶
一般情况下,你应该在app下的models.py中创建Model。从uliweb.orm中导入所有东西,然 后创建自已的Model,它应该从 Model
类进行派生。然后添加你想要定义的字段。例如:
from uliweb.orm import *
import datetime
class Note(Model):
username = Field(CHAR)
message = Field(TEXT)
homepage = Field(str, max_length=128)
email = Field(str, max_length=128)
datetime = Field(datetime.datetime, auto_now_add=True)
表名¶
缺省情况下,表名应该是Model类名的小写。比如上面的Note的表名应该是 note
。
如果你想设置为其它的表名,你可以在Model类中定义一个 __tablename__
,例如:
class Note(Model):
__tablename__ = 't_note'
如果存在通用的表名定义规则,可以考虑使用配置参数 TABLENAME_CONVERTER
。
表的映射(0.1.7新増)¶
什么叫表的映射,就是它只是现有表的一个映射,在执行syncdb, reset, alembic相关命令 时,不会在数据库中执行create table或drop table的操作。因此,它只是用来映射。
这里,映射的表,可以是真正的表,或者是视图(View)。
它的作用?比如有两个项目,其中一个项目功能比较简单,它将使用另一个项目的表,因此 它本身并不需要建表,只要映射就可以了。因此它只需要把另一个项目的App添加到项目中。 另外,在使用alembic时,因为数据库的表是 由两个项目组成的,所以需要某种方式来区分:哪些表要真正创建,哪些表是已经存在直接 映射即可。因此Uliweb 0.1.7就引入了 __mapping_only__
属性。需要时,在Model的类 属性中定义即可,如:
class User(Model):
__mapping_only__ = True
不过以上的方式是直接配置到代码中,为了更灵活,也可以在settings.ini中进行配置:
[MODELS_CONFIG]
user = {'__mapping_only__':True}
表参数¶
在SQLAlchemy中,当你创建一个表时,你可以传入一些额外的参数,例如: mysql_engin等。 所以,你可以在Model类中定义 __table_args__
,例如:
class Todo(Model):
__table_args__ = dict(mysql_charset='utf8')
如在MySQL中修改某张表的存储引擎,可以:
__table_args__ = {'mysql_engine':'MyISAM'} #'InnoDB'
常见的参数定义为:
__table_args__
将传给底层的 Table 的定义,具体内容符合 SQLAlchemy 的要求
__mapping_only__
用来定义表的映射状态,详情见上面的说明。
__tablename__
用来定义数据库的表名。缺省情况下,表名是 Model 的类名小写形式。如果Model与 数据库中的表名不同,则可以通过这个属性定义数据库中的名字。在使用时,仍然应 使用配置名,而不是表名。
__dispatch_enabled__
用来设置是否发出信号,如:
post_save
,pre_save
,pre_delete
等。在不希 望有人处理这些事件时考虑设置。还可以通过调用set_dispatch(False)
来实现全 局性的禁止,主要是用在批处理中。__cacheable__
在使用 get() 方法时,如果
__cacheable__
为 True,则自动进行缓存处理,如使 用uliweb内置的 objcache APP 来处理。则get(id)
相当于get(id, cache=True)
注意,这些参数都可以定义在 settings.ini 中。
OnInit 方法¶
uliorm也允许你在创建表之时在一些初始化工作。只要写一个OnInit的class method,例 如:
class Todo(Model):
@classmethod
def OnInit(cls):
Index('my_indx', cls.c.title, cls.c.owner, unique=True)
上面的代码是用来创建复合索引。一般的单字段索引,可以在定义字段时直接指定Index=True。
default_query 方法¶
uliorm目前支持用户自定义缺省条件,即在查询时,会自动将缺省条件与输入的条件合并 处理,它需要定义为一个类方法,如:
class Todo(model):
@classmethod
def default_query(cls, query):
return query.filter(xxx).order_by(yyy)
default_query 将传入一个query对象,你可以对它使用Result上的查询相关的处理,比如: filter
, order_by
, limit
, offset
等可以返回结果集的方法。
不过,一旦定义了 default_query 所有的 filter 查询都会使用这个结果,除非你使用 filter().without()
来显示指时不需要 default_query 的处理。
字段定义¶
uliorm中定义一个Model的字段为Property,但为了方便,uliorm还提供了Field函数。
所有的字段都是以Property结尾的类。下面是uliorm中的字段类:
'BlobProperty', 'BooleanProperty', 'DateProperty', 'DateTimeProperty',
'TimeProperty', 'DecimalProperty', 'FloatProperty',
'IntegerProperty', 'Property', 'StringProperty', 'CharProperty',
'TextProperty', 'UnicodeProperty', 'FileProperty', 'PickleProperty'
你可能认为它们不好记忆,所以你可以使用Field来定义。
Field是一个函数,它的第一个参数可以是内置的Python type,也可以是uliorm定义的特殊 类型。其它的参数是和对应的Property类一致的。它会根据你传入的Python type或特殊类 型来自动查找匹配的字段类。
Python type和字段类的对应关系为:
引用简写类型 | 实际类型 |
str | StringProperty, |
CHAR | CharProperty, |
unicode | UnicodeProperty, |
TEXT | TextProperty, |
BLOB | BlobProperty, |
FILE | FileProperty |
int | IntegerProperty, |
float | FloatProperty, |
bool | BooleanProperty, |
datetime.datetime | DateTimeProperty, |
datetime.date | DateProperty, |
datetime.time | TimeProperty, |
decimal.Decimal | DecimalProperty, |
DECIMAL | DecimalProperty, |
PICKLE | PickleProperty, |
小写的,都是Python内置的类型或类。大写的都是uliorm为了方便记忆而创建的。而上面 看到的关于Node的示例就是使用Field来定义字段的。
ID 属性¶
缺省情况下,uliorm会自动为你添加一个 id
字段,而你并不需要在Model中进行定义。
如果你不想自动定义ID,则可以在Model添加一个类属性 __without_id__
则 uliorm 将不会自动创建 id 属性。
Property 构造函数¶
Property 其它所有字段类的基类。所以它的一些属性和方法将会被派生类使用到,它的定 义为:
Property(verbose_name=None, fieldname=None, default=None, required=False,
validators=None, choices=None, max_length=None, hint='',
auto=None, auto_add=None, type_class=None,
type_attrs=None, placeholder='', extra=None,
sequence=False, server_default=None,
nullable=__nullable__, index=None, unique=False,
primary_key=False, autoincrement=False)
- verbose_name
用于显示字段的描述信息。一般是用在显示界面上。
- fieldname
数据库字段名。缺省情况下和Property的实例名相同。例如:
class User(Model): username = StringProperty(fieldname='user_name')
username就是Property的实例名,而fieldname缺省不给出的话就是
username
, 上面的 示例是指定了一个不同的值。因此你通过orm引用属性时要使用username
,但是 直接对数据库查询或操作时,即要使用user_name
。在特殊情况下代码中的字段名可能 和数据库中的字段名不同。原来此参数名为 name ,后为了清晰表示fieldname,增加了fieldname参数和属性,原来 的name将不允许直接赋值,它将直接和Property属性的名字相同。主要是在直接处理Property 实例时,可以根据name来获得Model字段的名字。
- default
字段的缺省值。注意,default可以是一个函数。在创建一个Model的实例时,对于未 给出值的属性,uliorm会自动使用default给字段赋值。因此,如果default没有赋值, 则这个值一般为None。但是对于象IntegerProperty之类的特殊字段来说,缺省值不是None,如
0。同时,在调用时要注意default函数执行是否可以成功。因为有的时候需要 在某个环境下,而你在执行时可能不具备所要求的环境,比如default函数要处理request.user, 但是你有可能在批处理中去创建实例,这样request.user是不会存在的,因此会报错。 简单的处理就是把Model.field.default置为None。
default 并不影响建表语句,所以如果想实现建表的default定义,需要使用
server_default
。- required
指明字段值是否不能为None。如果在创建Model实例时,没有传入required的字段值, 则uliorm会检查出错。同时这个属性可以用在Form的处理中。
- validators
当给一个属性赋值时,uliorm会根据这个参数来校验传入值的合法性。它应该是一个 函数,这个函数应写为:
def validator(data): xxx if error: raise BadValueError, message
如果校验失败,这个函数应该抛出一个 BadValueError的异常。如果成功,则返回 None或不返回。
- hint
用来定义一个帮助信息,可以用在form中作为Form字段的help_string的值。
- auto
可以是一个函数。表示在Update时,如果没提供值则自动设置auto。
- auto_add
与 auto 类似,不同之处在于,它只是在Insert时起作用。default的取值是不关心作什么操作的,而 auto 和 auto_add 要关心具体的操作。
- choices
当属性值的取值范围是有限时可以使用。它是一个list,每个元素是一个二元tuple, 格式为(value, display),value为取值,display为显示信息。目前,uliorm并不用 它来校验传入数据的正确性,用户可以根据需要自定义校验函数,传入validators中 进行校验处理。
- max_length
字段的最大长度,仅用在
StringProperty
,CharProperty
中。如果没 有指定缺省为255。- sequence
用在postgresql数据库中,表示一个sequence字段。
- index
如果设置为True则表示要使用当前字段生成索引。只适合单字段索引。如果要生成复 合索引,要生成OnInit类方法,并调用Index函数来生成。缺省为False。
- unique
表示字段是否可以重复。缺省为False。
- nullable
指示在数据库中,本字段是否可以为
NULL
。缺省为True。- type_class, type_attrs
可以用来设置指定的SQLAlchemy的字段类型并设置要传入的字段属性。如果有长度值, 则是在max_length中指定。
- server_default
数据库缺省值,它会影响创建表时的Create语句,它会生成
DEFAULT
子句。它的取 值按SQLAlchemy的写法应该使用text来封装,如text(0)
。对于数值类型,正确的写法是使用text()
来封装,不过Uliorm作了处理,可以直接使用数值。如:server_default=0
。
关于nullable和server_default在settings.ini中有配置项可以进行全局缺省值的设置:
[ORM]
NULLABLE = True
SERVER_DEFAULT = False
所以在缺省情况下,当没有给字段值的时候,如果default为None并且server_default也为 None,则存入数据库时会是NULL。如果你可以根据需要来修改这个全局配置,或针对每张表 通过参数来调整。
字段列表¶
CharProperty¶
与 CHAR
相对应。你应该传入一个 max_length
。如果传入一个Unicode字符串它 将转换为缺省编码(utf-8)。
StringProperty¶
与 VARCHAR
相对应。你应该传入一个 max_length
。如果传入一个Unicode字符串它 将转换为缺省编码(utf-8)。目前uliorm从数据库中取出StringProperty时会使用Unicode, 而不转换为utf-8或其它的编码。因此与UnicodeProperty是一致的。
TextProperty¶
与 TEXT
相对应。用于录入大段的文本。
UnicodeProperty¶
与 VARCHAR
相对应。但是你需要传入Unicode字符串。
BlobProperty¶
与 BLOB
相对应。用于保存二进制的文本。
DateProperty DateTimeProperty TimeProperty¶
这些字段类型用在日期和时间类型上。它们还有其它的参数:
- auto_now
当设置为True时,在保存对象时,会自动使用当前系统时间来更新字段的取值。
- auto_now_add
当设置为True时,仅创建对象时,会自动使用当前系统时间来更新字段的取值。
- format
用来设置日期时间的格式串,uliorm会用它进行日期格式的转换。在缺省情况 下,当传入一个字符串格式的日期字段时,uliorm会进行以下尝试:
格式串 样例 '%Y-%m-%d %H:%M:%S' '2006-10-25 14:30:59' '%Y-%m-%d %H:%M' '2006-10-25 14:30' '%Y-%m-%d' '2006-10-25' '%Y/%m/%d %H:%M:%S' '2006/10/25 14:30:59' '%Y/%m/%d %H:%M' '2006/10/25 14:30' '%Y/%m/%d ' '2006/10/25 ' '%m/%d/%Y %H:%M:%S' '10/25/2006 14:30:59' '%m/%d/%Y %H:%M' '10/25/2006 14:30' '%m/%d/%Y' '10/25/2006' '%m/%d/%y %H:%M:%S' '10/25/06 14:30:59' '%m/%d/%y %H:%M' '10/25/06 14:30' '%m/%d/%y' '10/25/06' '%H:%M:%S' '14:30:59' '%H:%M' '14:30'
BooleanProperty¶
与 Boolean
相对应。不过对于不同的数据库底层可能还是不同。具体是由SQLAlchemy 来实现的。
DecimalProperty¶
与 Numric
相对应。它有两个参数:
- precision
总长度,不计算小数点位数。
- scale
小数长度。
FloatProperty¶
与 Float
对应。它有一个参数:
- precision
总长度。
IntegerProperty¶
与 Integer
对应。
FileProperty¶
与 VARCHAR
对应。用于保存文件名,而不是文件对象。缺省的max_length为255。
PickleProperty¶
有时我们需要将一个Python对象保存到数据库中,因此我们可以采用 BLOB
字段来处理。 首先将对象序列化为字符串,可以使用Python自带的pickle,然后写入数据库。读出时再 反序列化为Python的对象。使用 PickleProperty
可以把这一过程自动化。
Model的常见属性¶
- table
uliorm的Model对应于SQLAlchemy的
Table
对象,而table
将是底层的 Table的实例。所以你可以使用这个属性来执行表级的操作。- c
Model的字段集。与 table.c 属性是一样的。
- properties
所有定义在Model中的属性。
- metadata
与SQLAlchemy中的metadata相对应的实例。
- tablename
表名。
关系定义¶
uliorm支持以下几种关系的定义: OneToOne, Reference, SelfReference, ManyToMany.
OneToOne¶
OneToOne是用来定义一对一的关系。
>>> class Test(Model):
... username = Field(str)
... year = Field(int)
>>> class Test1(Model):
... test = OneToOne(Test)
... name = Field(str)
可以使用OneToOne的关系来直接引用另一个对象。例如:
>>> a1 = Test(username='limodou')
>>> a1.save()
True
>>> b1 = Test1(name='user', test=a1)
>>> b1.save()
True
>>> a1
<Test {'username':'limodou','year':0,'id':1}>
>>> a1.test1
<Test1 {'test':<Test {'username':'limodou','year':0,'id':1}>,'name':'user','id':1}>
>>> b1.test
<Test {'username':'limodou','year':0,'id':1}>
在定义OneToOne时,可以传入一个collection_name的参数,这样,可以用这个名字来反向 引用对象。如果没有给出collection_name,则将使用表名作为引用名。
注意,OneToOne只是一个关系,它并不会自动根据主表记录自动创建关联表的记录。
Reference¶
uliorm使用 Reference
来定义多对一的关系。
>>> class Test(Model):
... username = Field(str)
... year = Field(int)
>>> class Test1(Model):
... test = Reference(Test, collection_name='tttt')
... name = Field(str)
>>> a1 = Test(username='limodou1')
>>> a1.save()
True
>>> b1 = Test1(name='user', test=a1)
>>> b1.save()
True
>>> b2 = Test1(name='aaaa', test=a1)
>>> b2.save()
True
>>> a1
<Test {'username':'limodou1','year':0,'id':1}>
>>> list(a1.tttt.all())[0] #here we use tttt but not test1_set
<Test1 {'test':<Test {'username':'limodou1','year':0,'id':1}>,'name':'user','id':1}>
>>> a1.tttt.count()
2
上面的例子演示了多个Test1记录可能对应一个Test记录。因此,我们可以在Test1中 定义 Reference
到Test上。对于Test1的某个实例,假定为b1,我们就可以通过 b1.test来获得对应的Test对象。这里会自动引发一个查询。如果你想从Test的某个对 象来反向获取Test1应该怎么办呢?假定Test的对象实例为a1,则缺省情况下我们可以通 过a1.test1_set.all()来获得a所对应的所有Test1的实例。为什么是all()呢?因为一个 Test对象有可能对应多个Test1对象(这就是多对一关系),所以得到的可能不仅一条 记录,应该是一个结果集。再看一下 test1_set
,它就是Test1的表名加 _set
后缀。但是,如果Test1中有多个字段都是到Test的Reference会出现什么情况。这时, Uliweb会抛出异常。原因是,这样会在Test类中出现多个同名的test1_set属性,这是 有冲突的。所以当存在多个到同一个表的引用时,要进行改名。而Reference提供了一个 collection_name
的参数,可以用它来定义新的别名。比如上面的 tttt
。这样 在获取a1所对应的Test1的记录时,就可以使用 a1.tttt
来反向获取了。
Refernce有以下几个参数可以使用:
- reference_class
第一个参数,指明要关联的Model。可以是Model类,也可以是字符串形式的表名。 如果是第二种用法,则要与get_model配合使用。详见get_model的用法说明。
- collection_name
前面已经介绍,是反向获取记录的名字
- verbose_name
字段的提示信息
- reference_fieldname
当引用一个Model时,缺省情况下是使用该Model的id字段。但是在特殊情况下,你可 能希望指定其它的字段。这样可以将要引用的字段名传给
reference_fieldname
参数。这样uliorm会根据被引用的字段来动态创建字段的类型。- required
是否是必输项。缺省为False。
uliorm的Reference关系并不会生成ForeignKey的外键。因为,一旦使用外键,则删除 导入数据时都有一个执行顺序,非常难处理。所以在设计上没有采用外键。
SelfReference¶
如果你想引用自身,你可以使用 SelfReference
, 例如:
>>> class User(Model):
... username = Field(unicode)
... parent = SelfReference(collection_name='children')
ManyToMany¶
>>> class User(Model):
... username = Field(CHAR, max_length=20)
... year = Field(int)
>>> class Group(Model):
... name = Field(str, max_length=20)
... users = ManyToMany(User)
>>> a = User(username='limodou', year=5)
>>> a.save()
True
>>> b = User(username='user', year=10)
>>> b.save()
True
>>> c = User(username='abc', year=20)
>>> c.save()
True
>>> g1 = Group(name='python')
>>> g1.save()
True
>>> g2 = Group(name='perl')
>>> g2.save()
True
>>> g3 = Group(name='java')
>>> g3.save()
True
>>> g1.users.add(a)
>>> g1.users.add(b)
你可以使用 ManyToMany
来指明一个多对多的关系. uliorm会象Django一样自动创建 第三张表,上例的第三张表会是: group_user_usres
, 它是由两个表名(user和group) 和关系名(users)组成. 第三张表的表结构会是:
CREATE TABLE group_user_users (
group_id INTEGER NOT NULL,
user_id INTEGER NOT NULL,
PRIMARY KEY (group_id, user_id)
)
手工创建 ManyToMany 表¶
在 Uliorm 中支持两种创建 ManyToMany 表的机制,一种是最常见的自动创建的方式。只要定义关系,就可以 自动创建。但是这种情况下,ManyToMany 表只会有两个字段。如果我们还希望在这个表上添加其它的字段,就 需要手工创建这张表,然后将其关联到 ManyToMany 中。
例如,先定义两个 User, Group Model:
class User(Model):
username = Field(str)
class Group(model):
name = Field(str)
然后定义第三张表,如: User_Group_Rel:
class User_Group_Rel(Model):
user = Reference('user')
group = Reference('group')
role = Field(CHAR)
这里第三张表要定义两个 Reference 字段,一个指向 User ,一个指向 Group。然后我们可以把这个关系放 在 User 或 Group 上,修改 User 为:
class User(Model):
username = Field(str)
groups = ManyToMany('group', through='user_group_rel',
through_reference_fieldname='user',through_reversed_fieldname='group')
通过 through
来定义将要引用的表名,通过 through_reference_fieldname
定义与 User.id 对应的 关系字段名,通过 through_reversed_fieldname
定义与 Group.id
对应的关系字段名,相当于要使用 以下的连接条件:
user.id==user_group_rel.user and group.id==user_group_rel.group
如果你的两个关系字段分别定义为对应的 <表名>_id
的形式,如:
class User_Group_Rel(Model):
user_id = Reference('user')
group_id = Reference('group')
role = Field(CHAR)
那么将不需要设置 through_reference_fieldname
和 through_reversed_fieldname
。
定义好之后,就可以象一般 ManyToMany 字段一样来使用了。
但是我们定义单独的 ManyToMany 表是为了能够在关系上保存额外的数据,因此为了得到它们,需要在查询时指定 `with_relation(relation
操作¶
ORM的操作可以分为不同的级别: 实例级、Model级和关系级。
- 实例级
这类操作只会影响实例自身,你可以进行: 创建、获取、删除、更新等操作。
- Model级
这类操作所处理的范围是整个Model或表级,它主要进行集合性质的操作。你可以进行: 查询、计数、排序、删除、分组等操作。
- 关系级
不同的关系可以执行不同的操作。如:OneToOne可以进行实例级操作。而Reference, SelfReference和ManyToMany则可以进行集合操作。在使用关系时,一种我们是使用 inst.relationship的方式,这样会自动将关系与正在处理的实例进行条件的绑定, 另一种是通过Model.relationship的方式,这样可以调用关系字段的某些特殊方法, 比如用来生成条件。
实例级¶
创建实例¶
假定有一个 User Model,类的定义为:
class User(Model):
username = Field(CHAR, max_length=20)
year = Field(int)
所以,如果你想要创建一个User的实例,只要:
user = User(username='limodou', year=36)
但这样还不会保存到数据库中,它只是创建了一个实例,你还需要调用 save
来保存:
user.save()
获取实例¶
user = User.get(5)
user = User.get(User.c.id==5)
可以通过Model.get()来获取一个实例。在get()中是条件。如果是一个整数,则认为是要 获取id等于这个值的记录。否则你可以使用一个条件。这里条件的写法完全是遵守 SQLAlchemy 的要求。如果条件不止一个,可以使用 and_, or_, not_
或 &, |, ~
来拼接条件。SQLAlchemy 的相关文档可以查看: http://www.sqlalchemy.org/docs/core/tutorial.html
注意,在结果集上,你可以多个使用filter()连接多个 and
的条件,而get不支 持这样的用法。比如你可以 User.filter(User.c.id=5).filter(User.c.year>30)。
user = User.get_or_notfound(5)
使用get_or_notfound可以当无满足条件的对象时抛出一个NotFound的异常。
删除实例¶
user = User.get(5)
user.delete()
delete在删除对象时,会自动删除相关联的ManyToMany的关系数据。如果不想删除,则可以 传入 manytomany=False
。
更新实例¶
user = User.get(5)
user.username = 'user'
user.save()
更新实例可以直接向实例的某个字段赋予新值,也可以使用update方法来一次更新多个字 段。如:
user.update(username='user')
user.save()
注意,象创建和更新时,在调用相关的方法时,你传入的是key=value的写法,这里 key就是字段的名字。但是在写条件时,你要使用 Model.c.fieldname 这样的写法, 并且不是赋值,而是python的各种运算符。不要搞错了。
Uliorm在保存时会根据对象的id值是否为None来判断是否是insert还是update。如果你直接 设置了id值,但是又希望通过insert来插入数据,可以在调用save时传入 insert=True
。
Model中更新数据库相关的方法,如: save, delete, get, get_or_notfound, count, remove 都可以传入connection参数,它可以是数据库连接名或真正的连接对象。
其它的API¶
- to_dict(fields=[], convert=True, manytomany=False)
将实例的值转为一个dict对象。如果没有给出fields参数,则所有字段都将转出。 注意,这里对
ManyToMany
属性有特殊的处理。因为ManyToMany
属性并 不是真正的表中的字段,所以缺省情况下是不会包含这些值的,如果指定manytomany为 True,则会也把相应的ManyToMany
所对应的对象集的ID取出来,组织为一个list。 如果convert=True,则在取出字段值的时候,还会调用field_str函数进行值的处理。 在调用field_str时,strict保持为False不变。 举例:a = User.get(1) a.to_dict() #this will dump all fields a.to_dict(['name', 'age']) #this will only dump 'name' and 'age' fields
- field_str(v, strict=False)
将某个字段的值转为字符串表示。如果strict为False,则只会处理日期类型、Decimal 类型和将Unicode转为字符串。如果strict为True,则:None会转为'',其它的全部转为 字符串。
- get_display_value(field_name)
返回指定字段的显示值。特别是对于包含有choices的字段,可以根据相应的值返回对 应的choices的值。
- get_datastore_value(field_name)
返回指定字段的数据库的值。特别是对于
Reference
字段,如果直接使用inst.reference 则得到的会是引用的对象,而不是数据库保存的值。而使用get_datastore_value()
可以得到数据库的值。uliorm会将
Reference
字段保存到_field_name_
的属性中,因此可以 直接使用它来得到Reference
的值。比如User.c.system
可能是指向System
表的引用,直接使用user.system
会得到对象的System
的对象。而使用user._system_
则得到对应的数据库的值。
Model级¶
uliorm在Model级上的操作主要有两类,一类是直接通过Model.func来调用的,另一类是通 过Model.func或Model.relationship的方式返回结果集,再在结果集上进行操作。对于与 查询相关的函数,是可以连在一起使用的,比如:
User.filter(...).filter(...).count()
有些方法会返回结果集,因此你可以在返回值的基础上,再调用查询相关的方法。有些方法会 直接返回结果,不能再调用查询相关的方法。
查询¶
在查询一个表的时候可能会有两种需求:全部记录和按条件筛选,因此对应着可以使用 all()
和 filter()
。all()
中是没有参数的,它会返回一个 Result
对象,这是前面介绍的结果集,你可以在结果集上继续使用其它的方法。 filter()
需要传入条件,条件的写法是符合SQLAlchemy要求的。它也返回一个结果集。多个 filter()
是可以连接使用的,相当于多个与条件。 empty()
返回一个空的结果集.
举例:
User.all()
User.empty()
User.filter(User.c.year > 18)
删除记录¶
Model中提供了 remove(condition)
来删除满足条件的记录。同时你也可以利用结果 集来删除。例如:
User.remove(User.c.year<18)
#等价于
User.filter(User.c.year<18).remove()
注意,结果集的删除是使用 remove
,而实例的删除是使用 delete
。
记录条数统计¶
Model中提供了 count(condition)
来计算满足条件的记录数。同时你也可以利用结果 集来统计,例如:
User.count(User.c.year<18)
#等价于
User.filter(User.c.year<18).count()
注意在使用count时,uliweb自动根据查询中是否存在:limit, group_by, join来决定是直接使用 select count(*) from table where condition
的形式,还是采用 select count(*) as count_1 from (select * from table where)
的形式。因为,一旦存在 limit, group_by, join,在计数时统计的是结果集,而第一种只是根据条件来处理的。所以对于 limit 这样的查询要按 结果集来统计。同时要注意,因为是对结果集来统计,所以它是先进行了一个子查询,从总体效果上来看,速度会慢一些。
树型关系处理¶
有时我们需要在数据库中通过二维表来定义树型关系,比如定义 parent
字段表示父结点。为了省事,UliORM 提供了 get_tree
方法可以方便遍历树型关系。
基本使用如:
Depart.get_tree(parent=None)
上述例子表示,遍历父结点为 None
的树形结构。返回值是一个generator,可以使用for循环来获取结果。
完整的API为:
get_tree(condition,
parent_field, #父结点字段名,缺省为 `parent`
parent, #父结点的值
parent_order_by, #父结点查询排序字段
current, #当前结点的值
order_by, #子结点查询排序字段,
id_field, #current值对应的字段名
mode, #遍历顺序 'wide' 广度优先, 'deep' 深度优先,缺省为 'wide'
)
get_tree
的工作分为两步:
查询父结点。这里有两种方式:
- 指定
parent
字段来查询,因此会用到parent_field
和parent
参数 - 指定id为某个值,因此会用到
current
和id_field
对于更加复杂的情况,可以传入parent参数, 它可以是一个条件
- 指定
查出父结点之后,再递归查出子结点。
condition
是用在查子结点的。parent_order_by
用在得到父结点时对父结点进行排序。
其它 API¶
- bind(metadata=None, auto_create=False)
绑定当前的类到一个metadata对象上。如果
auto_create
为True
, 则将 自动建表。- create()
建表,并且会自动检查表是否存在。
- connect()
切換数据库连接,这样后续的执行将在新的数据库连接上进行。
- get_engine_name()
获得当前表所使用的数据库连接的名字。在多个地方都可以设置数据库连接,uliweb 将按以下顺序来判断:
- 是否设置了
__engine_name__
- 是否在
settings.ini
中设置了对应的连接名 'default'
这样在缺省情况下,数据库连接名为
default
.- 是否设置了
关系级¶
一对一(One to One)¶
一对一关系没什么特别的,例如:
>>> class Test(Model):
... username = Field(str)
... year = Field(int)
>>> class Test1(Model):
... test = OneToOne(Test)
... name = Field(str)
>>> a = Test(username='limodou', year=36).save()
>>> b = Test1(name='user', test=a).save()
>>> b.test
<Test {'username':'limodou', 'year':36}>
所以你可以使用 b.test
如同 a
对象。
注意,关系的建立是在相关的对象创建之后,而不是会根据关系自动创建对应的对象。
多对一(Many to One)¶
>>> class Test(Model):
... username = Field(str)
... year = Field(int)
>>> class Test1(Model):
... test = Reference(Test, collection_name='tttt')
... name = Field(str)
>>> a = Test(username='limodou').save()
>>> b = Test1(name='user', test=a).save()
>>> c = Test1(name='aaaa', test=a).save()
根据上面的代码, Test:Test1 是一个 1:n 关系。并且 b.test
是对象 a
。但是 a.tttt
将是反向的结果集,它可能不止一个对象。所以 a.tttt
将返回一个 Result
对象。并且这个结果集对象将绑定到 Test1 Model,所以结果集的 all()
和 filter()
方法将只返回 Test1 对象。更多的细节可以查看 Result
的描述。
多对多(Many to Many)¶
>>> class User(Model):
... username = Field(CHAR, max_length=20)
... year = Field(int)
>>> class Group(Model):
... name = Field(str, max_length=20)
... users = ManyToMany(User)
>>> a = User(username='limodou', year=5).save()
>>> b = User(username='user', year=10).save()
>>> c = User(username='abc', year=20).save()
>>> g1 = Group(name='python').save()
>>> g2 = Group(name='perl').save()
>>> g3 = Group(name='java').save()
>>> g1.users.add(a)
>>> g1.users.add(b)
当你调用 a.group_set
(因为你没有在ManyToMany属性中定义collection_name)或 g1.users
时,将返回一个 ManyResult
对象。
Result 对象¶
Result
对象的生成有多种方式,一种是执行某个关系查询时生成的,一种是直接在 Model上调用 all()
或 filter()
生成的。Result
对象有多个方法可以调 用,有些方法,如 filter()
会返回 Result
本身,因此还可以继续调用相应的 方法。有些方法直接返回结果,如: one()
, count()
。因此你可以根据不同的 方法来考虑是不是使用方法的连用形式。
注意, Result
对象在调用相应的方法时,如果返回的是结果集本身,此时不会立即 进行数据库的交互,而是当你调用返回非结果集的函数,或要真正获得记录时才会与数据 库进行交互。比如执行 User.filter(...).count()
时,在执行到User.filter(...) 并没有与数据库进行交互,但在执行到 count() 时,则生成相应的SQL语句与数据库进行 交互。又如:
query = User.all()
for row in query:
在执行 query = User.all()
时,并不会引发数据库操作,而在执行 for
语句时 才会真正引发数据库的操作。
同时, Result
在获取数据时,除了 one()
和 values_one()
会直接返回 一条记录或 None。all()
, filter()
, values()
会返回一个 generator。 所以如果你想要一个list对象,需要使用 list(result) 来转成 list 结果。
方法说明:
- all(): Result
返回Result本身. 注意在 Model中也有一个all()方法,它就是创建一个
Result
对象,然后将其返回。如果不带任何条件创建一个结果集,则在处理记录时相当 于all()的调用。- empty(): Result
返回空的结果集.
- any(): Boolean
根据条件判断是否存在相应的记录.如果存在返回
True
,不存在返回False
.- filter(condition): Result
按条件查询。可以多个filter连用。返回结果集本身。 示例:
User.filter(User.c.age > 30).filter(User.c.username.like('Lee' + '%%'))
- connect(engine_name): Result
切換到指定的连接名上,engine_name可以是连接名,Engine对象或Connection对象。
- count(): int
返回满足条件的记录条数。需要与前面的all(), filter()连用。
在Model中也有一个count()方法,但是它是可以带条件的,比如:
User.count(User.c.age > 30)
。 它可以等同于User.filter(User.c.age > 30).count()
示例:
User.all().count() User.filter(User.c.username == 'a').count()
- remove(): None
删除所有满足条件的记录。它其实是调用 Model.remove(condition)。可以和
all()
和filter()
连用。- update(**kwargs):
执行一条update语句。例如:
User.filter(User.c.id==1).update(username='test')
它等同于:
do_(User.table.update().where(User.c.id==1).values(username='test'))
- order_by(*field): Result
向查询中添加
ORDER BY
字句。例如:result.order_by(User.c.year.desc()).order_by(User.c.username.asc()) #or result.order_by(User.c.year.desc(), User.c.username.asc())
缺省情况下是按升序排列,所以asc()可以不加。
- limit(n): Result
向查询中添加
LIMIT
子句。n
是一个整数。- offset(n): Result
向查询中添加
OFFSET
子句。n
是一个整数。- distinct(*field): Result
向查询中添加
DISTINCT
函数,field是字段列表。- values(*fields): 结果 generator
它将根据前面设置的条件立即返回一个结果的generator。每行只会列出指定的字段值。 fields为字段列表,可以直接是字段的名字,也可以是Model.c.fieldname的形式。 例如:
>>> print a1.tttt.all().values(Test1.c.name, Test1.c.year) [(u'user', 5), (u'aaaa', 10)] >>> print a1.tttt.all().values('name', 'year') a1.tttt.all().values(Test1.c.name, Test1.c.year)
- one(): value
只返回结果集中的第一条记录。如果没有记录,则返回
None
。- values_one(*fields): value
相当于执行了
values()
, 但是只会返回第一条记录。- get(condition): value
相当于
Result.filter(condition).one()
。- without(flag='default_query')
去掉default_query的条件处理。
ManyResult¶
ManyResult
非常象 Result
, 只不过它是通过 ManyToMany
关系创建的,它 拥有与 Result
大部分相同的方法,但是有一些差别:
- add(*objects): boolean
这个方法可以建立多个对象与当前对象的多对多关系。其实就是向第三张关系表中插入 相应的记录。它会返回一个boolean值。如果为 Ture 表示有变化。否则无变化。如果 Model A的实例a已经和Model B的某些实例有多对多的关系,那么当你添加新的关系时 对于已经存在的关系将不会再添加,只添加不存在的关系。
- update(*objects): boolean
这个方法与add()有所不同。add会在原来的基础之上添加新的关系。而update会完全 按照传入的对象来重新修改关系,对于仍然存在的关系将保留,对于不存在的关系将 删除。它也会返回是否存在修改的状态。
- ids(): list
它将返回ManyToMany关系中所有记录的 ID 列表。注意,这里的ID是与定义ManyToMany 属性时所使用的引用字段一致的。缺省情况下是id字段,如果使用了其它的引用字段 则有可能是别的字段。
- has(*objects): boolean
判断传入的对象是否存在于关系中。这里对象可以是对象的id值,也可以是对象。如果 存在则返回 True,如果不存在则返回 False。
Session管理¶
当我们需要进行数据库的操作时,我们要建立一个连接对象。在一个engine对象上,可以 建不同的连接对象,一个连接对象可以有不同的事务。因此事务都是放在某个连接对象上的。 为了方便使用这些连接对象,Uliweb对其进行了包装,构造了 Session 类。这个 Session 和SQLalchemy提供的 session 机制是不同的。在Uliweb主要是管理连接的,它还提供了事务 的管理功能。
Session对象会有两种创建方式,一种是自动创建。当我们在某个数据库连接上进行操作时, 如: do_(sql, engine_name)
,这里只指明了要操作的连接名。这种情况下,Uliorm会 自动使用对应连接名对象上的session对象(如果在执行SQL时还没有创建,则会自动创建)。 同时,考虑到多线程工作的情况,这个session对象在不同的线程环境是不同的。
所以这种情况下,当只使用连接名来进行SQL操作时,同一个线程使用的 Session 对象是 相同的,因此它们的事务也将是相同的。
第二种情况就是手工创建 Session 对象,只要执行 session = Session()
或 session = Session(engine_name)
会通过相应的数据库连接对象来创建相应的连接。这种方式是显示地创建 session 对象, 不会复用已经存在的 Session 对象。
事务处理¶
uliorm提供两种控制事务的方式,一种是通过Middleware,一种是手工处理。如果要使用 Middleware方式,你需要在settings.ini中添加:
MIDDLEWARE_CLASSES = [
'uliweb.orm.middle_transaction.TransactionMiddle'
]
使用Mideleware,它将在每个view处理时生效。当view成功处理,没有异常时,事务会被 自动提交。当view处理失败,抛出异常时,事务会被回滚。
一般情况下,只有事务处理Middleware捕获到了异常时,才会自动对事务进行回滚。 因此,如果你自行捕获了异常并进行了处理,一般要自行去处理异常。
手工处理事务,uliorm提供了基于线程模式的连接处理。uliorm提供了:Begin(), Commit(), 和Rollback()函数。当执行Begin()时,它会先检查是否当前线程已经存在一个连接, 如果存在,则直接使用,如果不存在则,如果传入了create=True,则自动创建一个连接, 并绑到当前的线程中。如果create=False,则使用engine的连接。同时Commit()和Rollback() 都会使用类似的方式,以保证与Begin()中获得的连接一致。
Web事务模式¶
一般你要使用事务中间件,它的处理代码很简单,为:
class TransactionMiddle(Middleware):
ORDER = 80
def __init__(self, application, settings):
self.db = None
self.settings = settings
def process_request(self, request):
Begin()
def process_response(self, request, response):
try:
return response
finally:
CommitAll(close=True)
if self.settings.ORM.CONNECTION_TYPE == 'short':
db = get_connection()
db.dispose()
def process_exception(self, request, exception):
RollbackAll(close=True)
if self.settings.ORM.CONNECTION_TYPE == 'short':
db = get_connection()
db.dispose()
当请求进来时,执行 Begin() 以创建线程级别的连接对象。这样,如果在你的 View中要手工处理事务,执行Begin()会自动使用当前线程的连接对象。
应答成功时,执行 CommitAll(close=True)
,完成提交并关闭连接。因为有可能存在 多个连接,所以使用CommitAll. 而在View中手动控制一般只要调用 Commit()
就可以了, 关闭连接交由中间件完成。
如果中间处理抛出异常,则执行 RollbackAll(close=True)
,回滚当前事务,并关闭 所有连接。而在View中手动控制,也只要简单调用 Rollback()
就可以了,关闭连接处理由 中间件完成。
在View中的处理,有几点要注意,Begin(), Commit(), Rollback() 都不带参数调用。 在Uliorm中,SQL的执行分两种,一种是直接使用ORM的API处理,还有一种是使用SQLAlchemy 的API进行处理(即非ORM的SQL)。为了保证正确使用线程的连接对象,ORM的API已经都使用 do_()
进行了处理。 do_()
可以保证执行的SQL语句在当前的合理的连接上执行。几种 常见的SQL的书写样板:
#插入
do_(User.table.insert().values(username='limodou'))
#更新
do_(User.table.update().where(User.c.username=='limodou').values(flag=True))
#删除
do_(User.table.delete().where(User.c.username=='limodou'))
#查询
do_(select(User.c, User.c.username=='limodou'))
命令行事务模式¶
所谓命令行事务模式一般就是在命令行下运行,比如批处理。它们一般不存在多线程的环境, 所以一个程序就是一个进程,使用一个连接就可以了。这时我们可以还使用engine的连接 对象。使用时,只要简单的不带参数调用Begin(), Commit()和Rollback()就可以了。因为 Begin()在没有参数调用的情况下,会自动先判断有没有线程级的连接对象,这时一定是没有, 如果没有,则使用engine下的连接对象。
这样,SQL语句既可以使用do_()来运行,也可以使用原来的SQLAlchemy的执行方式,如:
#插入
User.table.insert().values(username='limodou').execute()
#更新
User.table.update().where(User.c.username=='limodou').values(flag=True).execute()
#删除
User.table.delete().where(User.c.username=='limodou').execute()
#查询
select(User.c, User.c.username=='limodou').execute()
NotFound异常¶
当你使用get_or_notfound()或在使用instance.refernce_field时,如果对象没找到则会 抛出NotFound异常。
Model配置化¶
uliorm在考虑Model的可替换性时,提供了一种配置机制。这种机制主要是由orm app来初 始化的,它对Model的编写有一定的要求。使用配置机制的好处主要有两点:
可以方便使用,不用关心要使用的Model是在哪里定义的。orm提供了
get_model()
方法,可以传入字符串的表名或真正的Model对象。因此在一般情况下,使用字符串 形式是最方便的。比如我们想获得一个User的Model,可以使用:User = get_model('user')
但是使用这种字符串的形式,对于Model的配置有要求。需要在settings.ini中配置:
[MODELS] user = 'uliweb.contrib.auth.models.User'
其中key为引用的别名。它可以是表名(一般为Model类名小写),也可以不是表名。 value为表所对应的Model类的路径。uliorm将在需要时自动进行导入。
为什么需要表名呢?因为orm提供的命令行工具中,syncdb会自动创建数据库中 不存在的表,它就是使用的真正的表名。
- 可以有条件的方便进行替换。 在某些时候,你可能发现某个app的表结构要扩展几个字段,但是因为已经有许多Model 和这个表实现了关联,而且这个app提供了其它与些Model相关的一些方法。因此,如果 简单地替换这个app,有可能会要同时修改其它的app的代码,比如导入处理等。如是你 在定义关系时使用的是get_model(name)的形式,并且name是字符串,这样你实际上已经 实现了Model的配置化。因此你就可以定义新的Model类,并且配置到settings.ini中来 替换原来的Model。如果不是把配置信息写到同一个settings.ini中,那么,你可以把 新的App定义到原来的App之后(这里指INSTALLED_APPS),这样后面定义的内容会覆盖前 面定义的内容。这种做比较适合扩展字段的情况,或表结构的修改不影响其它的功能调 用的情况。
在定义关系时,象OneToOne, Reference和ManyToMany时既可以接受字符串的Model名,也 可以直接传入Model的类,都可以。
如何在其它项目中使用 uliorm¶
uliorm是可以在非Uliweb项目和非web程序中使用的,因此根据是否有Uliweb项目,决定了 可以使用不同的方式。
非Uliweb项目¶
Uliweb项目中,所有的Model都要配置到settings.ini中去,所以在非Uliweb项目中,你无 法这样做,因此处理上会有所不同。因为没有了Model的配置,所以你需要在使用Model前 先导入它们。然后你要考虑是自动建表还是手工建表。我建议是把自动建表单独处理,只 在需要时执行。简单的一个代码示例:
from uliweb.orm import *
class User(Model):
name = Field(unicode)
class Group(Model):
name = Field(str)
users = ManyToMany(User, collection_name = 'groups')
if __name__ == '__main__':
db = get_connection('sqlite://')
db.metadata.drop_all()
db.metadata.create_all()
u1 = User(name='limodou')
u1.save()
g1 = Group(name='python')
g1.save()
g1.users.add(u1)
print g1.users.one().groups.one().users.one().name
print u1.groups.one().users.one().groups.one().name
这里 db.metadata.create_all()
用于创建所有的表。
Uliweb项目¶
如果我们要在非web程序中使用uliorm时,我们还是希望使用Uliweb的管理机制,使用Uliweb 项目的配置信息,这时我们可以:
from uliweb.manage import make_simple_application
app = make_simple_application(project_dir='.')
Begin()
try:
User = get_model('user')
print list(User.all())
Commit()
except:
Rollback()
在守护中使用Uliorm的注意事务¶
其实在守护中使用uliorm就是要注意使用事务。在我自已的开发中发现一个问题:
例如有一个循环,它的工作就是扫描数据库满足某个条件的数据集,如果有,则取出进行 处理,然后修改处理标志。处理完毕或不存在这样的数据,则sleep一定的时间。然后反复 执行。我在循环外创建一个数据库连接,这样可以复用这个连接。但是发现:一旦我在循 环中查到了数据,并执行了更新,则在以后的循环中,如果数据又发生了变化,但是我将 无法得到后来变化的数据。于是我到SQLAlchemy上问了一下,结果发现是由于数据库的连 接如果一直使用的话,当执行了更新,插入之类的操作后,事务隔离级会上升,造成数据 库认为当前连接所得到的数据已经是最新的,再查询时将不会返回新结果。所以,建议是 每次循环创建新的连接。因此,我在ORM中提供了Reset命令,它可以清除当前连接,从而 实现新连接的创建。因此如果你的处理是一个循环,可以在每次循环时执行 Reset()
。
信号处理¶
uliorm提供类似django信号的处理机制,它会在一些重要的执行点调用发出信号,以便让 其它的信号处理函数进行后续的工作。注意,uliorm的信号并不是真正的异步,它只是定 义上的异步,调用还是同步的。
预定义的几种信号¶
uliorm已经提供了几种预定义好的信号,下面列举出来。在每个信号名的冒号后面所定义 的是使用dispatch调用时使用的方法,分为call和get。其中call不需要返回值,并且会 将所有订阅此信号的方法依次调用。而get需要一个返回值,一旦某个方法返回非None的值, 则结束调用并将值返回。
- pre_save:call
保存一个对象 前 发出的信号 参数: instance, created, data, old_data
- instance
为保存的对象
- created
True为创建,False为修改
- data
新的数据
- old_data
旧的数据
- post_save:call
保存一个对象 后 发出的信号。参数同
pre_save
- pre_delete:call
删除一个对象 前 发出的信号 参数: instance
- instance
为待删除的对象
- post_delete:call
删除一个对象 后 发出的信号 参数: instance
- instance
为待删除的对象
- get_object:get
通过Model.get()获得一个对象 前 发出的信号。get_object和set_object 相结合可以实现简单的对get()方式的单对象的缓存处理。在uliweb中已经提供了一个 名为objcache的app,它可以在获取简单条件的对象时自动进行缓存的处理。 参数: condition
- condition
调用get()方法所使用的条件,它是SQLAlchemy的一个表达式对象
- set_object:call
通过Model.get()获得一个对象 后 发出的信号 参数: condition, instance
- condition
调用get()方法所使用的条件,它是SQLAlchemy的一个表达式对象
- instance
所获得的对象实例
定义接收函数¶
当使用uliorm时,它会根据执行情况自动发出相应的信号,此时如果有订阅此信号的方法存 在则将被自动调用,如果不存在,则继续后面的处理。在uliweb中,一般将订阅方法写在 settings.ini中,以减少启动时的导入处理。举例如下:
[BINDS]
audit.post_save = 'post_save'
audit.pre_delete = 'pre_delete'
在settings.ini中定义BINDS节,然后key是方法路径,值是对应的信号。方法路径的形式为:
module.function_name
为什么要这样定义?因为一个信号可以被多个方法来订阅,因此信号是可以重复的。
Uliweb在启动时会自动读取settings.ini中的信号,然后将其与相应的信号进行绑定。相 关的处理方法此时并不真正导入,而是当发出信号时,再动态导入。
接收函数的定义形式为:
def receiver(sender, topic, **kwargs)
第一和第二个参数都是固定的,sender是发出信号的对象。在uliorm中都是Model类。 topic是信号的名称。后面的kwargs对应每个信号可以接受的参数。不同的信号所接受的 参数可能是不同的。
测试代码¶
在 uliweb/test/test_orm.py 中有一些测试代码,你可以查看一些例子来了解如何使用 uliorm。