ORM基本使用

使用要求

需要安装sqlalchemy 0.7+以上的版本。如果你使用sqlite,则python 2.6+就自带了。如果 使用其它的数据库,则还需要安装相应的包。sqlalchemy本身是不带的。

配置

首先将 uliweb.contrib.orm 添加到 apps/settings.iniINSTALLED_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 syncdbuliweb 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的连接参数,可以选择的值为 plainthreadlocal. 其中 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_REVERSEFalse, 则并不会创建一个 (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和字段类的对应关系为:

引用简写类型实际类型
strStringProperty,
CHARCharProperty,
unicodeUnicodeProperty,
TEXTTextProperty,
BLOBBlobProperty,
FILEFileProperty
intIntegerProperty,
floatFloatProperty,
boolBooleanProperty,
datetime.datetimeDateTimeProperty,
datetime.dateDateProperty,
datetime.timeTimeProperty,
decimal.DecimalDecimalProperty,
DECIMALDecimalProperty,
PICKLEPickleProperty,
jsonJsonProperty,

小写的,都是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_fieldnamethrough_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 的工作分为两步:

  1. 查询父结点。这里有两种方式:

    1. 指定 parent 字段来查询,因此会用到 parent_fieldparent 参数
    2. 指定id为某个值,因此会用到 currentid_field

    对于更加复杂的情况,可以传入parent参数, 它可以是一个条件

  2. 查出父结点之后,再递归查出子结点。

    condition 是用在查子结点的。 parent_order_by 用在得到父结点时对父结点进行排序。

其它 API

bind(metadata=None, auto_create=False)

绑定当前的类到一个metadata对象上。如果 auto_createTrue, 则将 自动建表。

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的编写有一定的要求。使用配置机制的好处主要有两点:

  1. 可以方便使用,不用关心要使用的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会自动创建数据库中 不存在的表,它就是使用的真正的表名。

  2. 可以有条件的方便进行替换。 在某些时候,你可能发现某个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。