2005年12月24日

这其实并不是一个新的方法,它只是在 SQLAlchemy 中没有谈到罢了。它在 mapping/__init__.py 中。在 SQLAlchemy 的邮件列表中我对于 Table Metadata , User-defined class, Mapper 三者之间的关系产生了疑问,我认为 User-defined Class 作为 ORM 的主要承载者,它的功能似乎有些太简单了,没有特殊的基类,没有与某个 mapper 自动绑定,甚至需要我手工写,User.mapper = mapper(User, users),这样才把一个 mapper 与一个类相绑定,但使用时也不得不用 User.mapper 的方法。其实这一切都不需要这样,  SQLAlchemy 已经提供了一个非常方便的方法,那就是:

assign_mapper

它与一般的 mapper 没有什么区别,但却多做了一些与类绑定有关的工作,解决了我上面的不满。

它的代码也很简单,如:

def assign_mapper(class_, *args, **params):
    params.setdefault("is_primary", True)
    m = mapper(class_, *args, **params)
    class_.mapper = m
    class_.get = m.get
    class_.select = m.select
    class_.select_by = m.select_by
    class_.selectone = m.selectone
    class_.get_by = m.get_by
    def commit(self):
        objectstore.commit(self)
    def delete(self):
        objectstore.delete(self)
    class_.commit = commit
    class_.delete = delete

看到了吧,我可以直接从类上直接调用绑定的 mapper 的方法了,而且象 objectstore 的 commit(), delete() 方法也可以直接使用了。这样真是太方便了。调用与 mapper 方式一样,而且更简单:

assign_mapper(User, users)

2005年12月23日

这是在 SQLAlchemy 的邮件列表中看到 Michael Bayer 所发的一封邮件,真是个好消息。主要讲了新增加的功能。

Deferred Property Loading
————————————

就是在实现 mapper 时,可以指定某些字段是 Deferred 装入的,这样象通常一样取出数据时,这些字段并不真正的从数据库中取出,只有在你真正需要时才取出,这样可以减少资源的占用和提高效率。比如:

m = mapper(Order, orders, properties={
‘description’:deferred(orders.c.description)
})

只有在读取 description 时才会取出相应的数据。

Minimal Column Updates
———————————–

更新了 Update 语句,只有那些内容真正发生变化的列才会进行更新。这样就保证了那些被 Deferred 的列不会被无谓的装入。

Deferral Groups
———————-

Deferred字段可以通过在 properties 中指定 group参数来表示编组情况。这样当一个组的某个字段被取出时,同组的其它字段均被取出。挺有意思。

m = mapper(PressRelease, pressreleases, properties = {
‘body’ : deferred(pressreleases.c.body, group=’secondary’),
‘image’ : deferred(pressreleases.c.image_data, group=’secondary’),
‘credits’:deferred(pressrelease.c.credit, group=’secondary’),
})

比如在执行:

release = m.get_by(release_id=7)

时,所有Deferred的字段都没有取出来,但如果你执行:

print "body: " , release.body

则与 body 编在一组的其它字段(image, credits)都将被取出来。

非常好的改进,但也正如我在邮件中所说这样的话对于 User-defined class 的地位就变得重要了。

2005年12月18日

不是我不明白,这世界变化快。真的,我在 SQLAlchemy 中提了一些问题,比如关于文档中 OneToOne 等好象有问题,但 Mike 和 Robert 回信说这个问题应该已经改正的了。奇怪,于是我上到 SQLAlchemy 的主页上看了看,文档果然发生了变化。而我学习都是从前几天保存在 Scrapbook 中的页面中进行了。结果我比较了一下,果然是不一样,我的文档保存于 12/06 ,而最新的文档已经是 12/15 的了,不仅错误没有了,更增加了不少内容,这个一会儿会说。果然是世界变化快。

另外我提出增加 count() 方法的问题,得到 Mike 的积极响应。再有我不是很理解 OneToOne, OneToMang, MangToMang 在 Mapper 时如何区分,得到了很详细的回答,不过我还是不是特别理解。慢慢来吧。但发现 mapper 中增加了不少方法和文档。主要有下面的内容(从文档中拷贝):

# select_by, using property names or column names as keys
# the keys are grouped together by an AND operator
result = mapper.select_by(name=’john’, street=’123 green street’)

# get_by, which returns a single scalar result or None if no results
user = mapper.get_by(id=12)

# "dynamic" versions of select_by and get_by – everything past the
# "select_by_" or "get_by_" is used as the key, and the function argument
# as the value
result = mapper.select_by_name(‘fred’)
u = mapper.get_by_name(‘fred’)

# get an object directly from its primary key. this will bypass the SQL
# call if the object has already been loaded
u = mapper.get(15)

# get an object that has a composite primary key of three columns.
# the order of the arguments matches that of the table meta data.
myobj = mapper.get(27, 3, ‘receipts’)

# using a WHERE criterion
result = mapper.select(or_(users.c.user_name == ‘john’, users.c.user_name==’fred’))

# using a WHERE criterion to get a scalar
u = mapper.selectone(users.c.user_name==’john’)

# using a full select object
result = mapper.select(users.select(users.c.user_name==’john’))

# using straight text
result = mapper.select_text("select * from users where user_name=’fred’")

# or using a "text" object (the ‘engine’ parameter will not be needed soon)
result = mapper.select(text("select * from users where user_name=’fred’", engine=engine))

从上面可以看到主要增加了select_by() 和 get_by(),同时还有get()。select_by()可以简单地列出字符名,不用User.c之类的修饰,并且多个key关键字表示之间是 AND 的关系。select 返回的是列表。而为了满足返回单值的要求则提供了 get_XX 函数。get()是根据id返回一个对象,如果没有则为 None。而 get_by() 则表示多个字段同时满足的情况,同 select_by 一样,不用 User.c 之类的修饰了。那么,现在的 mapper 还提供了 select_by_XX() 和 get_by_XX() 方法,其中XX是字段名,如 get_by_name() ,那么它只接收对应字段的值,不用key参数的形式,更为方便。 select_text() 以前就有,但是原来在高级中讲述了,它可以完全使用字符串表示的 SQL 来处理。

在relation中有许多的属性可以使用,特别是有一个 userlist 参数:

uselist – a boolean that indicates if this property should be loaded as a list or a scalar. In most cases, this value is determined based on the type and direction of the relationship – one to many forms a list, one to one forms a scalar, many to many is a list. If a scalar is desired where normally a list would be present, set uselist to False.

你可以通过它来确保返回是列表还是单个对象。看说明是可以自动区分 one to many , one to one, many to many ,不过我还是不太清楚如何自动区分的。特别是 one to one!不过 Mike 在邮件列表中讲述了,我还没有消化掉。

今天还看到 Selectable 对象除了 execute() 可以调用外,还有一个叫scalar,它会返回结果集中的第一个,有时用起来会非常方便。今天是在邮件列表中的示例中看 Mike 用的。在 API 中发现的。如在 Rss Reader 中要判断否个feed有多少条未读的 POST,可以:

number = select([func.count(cls.c.id)], and_(cls.c.feed_id==feed_id, cls.c.read==False)).scalar()

2005年12月17日

我在 SQLAlchemy 中提了两个问题,一个是关于是否有方便的判断一个表是否存在的方法,另一个是如何使用事务。这两个问题都有回答。

对于第一个问题,我原以为可以简单的通过 engine 的 tables 属性(它是一个字典,key为表名)来判断是否存在表名。但其实是不可能用的。你必须要么定义了 Table Metadata 要么使用autoload来从数据库中反射,这样一个表才会在 tables 中存在。而一上来就使用 engine 对象,tables 是空的。Michael Bayer(SQLAlchemy的作者吧)给了我一个临时的解决方法:

t = Table(‘mytable’, engine, autoload=True)
exists = len(t.columns) > 0

也就是先通过 reflect 来得到一个 Table Metadata ,然后看一下字段个数是否大于0,如果是则表示存在。当然这个方法并不是很方便,希望可以有更方便的方法。

今天还同步了一下 SVN 发现不仅修改了我报告的问题,而且还在生成Column时可以指定default值(以前是不行了)。同时还增加了 Unicode 类型。这样与 SQLObject 就比较一致了。

我还提了一个如何 count 的方法,有人回答是这样的(在文档中有示例):

select([func.count(users.c.user_id)]).execute()

但这样并不方便,因为execute()返回的是一个结果集对象,需要使用fetchone()之类的语句再处理,然后得到一个列表,再得到确切的值,比如:

r = select([func.count(RssCategory.c.id)]).execute()
print r.fetchone()[0]

还是麻烦。

不过对于 SQLAlchemy 这种传统的 SQL 风格和 Data Mapping 风格混合在一起,而且可以方便使用感觉到还是很好的。在对ORM不方便或不是很理解时,可以使用 SQL 的方式。

Lazy Load

在实现了多个对象之间的关系之后,这里就存在了一个问题。比如A与B是一对多的关系。当取出A时,B不一定取出。只有在确切地通过A访问B时,B才会取出。那么这种做法就叫作 Lazy Load 。有时我们可能不想这样,希望把记录一下子全部取出,那么可以在 mapping 时象设置 private=True 一样增加一个参数:lazy=False。在缺省情况下Lazy是True。

m = mapper(Cate, RssCategory, properties = {
                    ‘feeds’ : relation(Feed, RssFeed, private=True,lazy=False)
                }
              )

在这里,文档中还提出了另一个问题,一旦我取出数据来放在了对象中,这里数据库发生了变化我如何才能刷新列表呢?那么一旦对象从数据库里取出来,它的装入已经完成,因此对于数据库的修改对已经装入的对象不再有影响。而且这样的问题已经涉及到事务处理了。

One to Many

一对多关系我们已经在前面看到的 RssCategory 和 RssFeed 就是这样。不过文档中并不是这样的。在前面的例子中,我们没有限定 private=True, lazy=False 的设定。同时,我们是在mapper统一设定 RssCategory 和 RssFeed 的关系。但在文档中是先实现了 addresses 的mapper,然后再将这个mapper用到 User  的 mapping 中去。转化为 RssCategory 和 RssFeed 的例子如下(完整的):

from SQLAlchemy import *
sqlite_engine = create_engine(’sqlite://filename=d:/test.db’, echo=True)

RssCategory = Table(‘rss_category’, sqlite_engine,
    Column(‘id’, Integer, primary_key = True),
    Column(‘title’, String, nullable = False)
)

RssFeed = Table(‘rss_feed’, sqlite_engine,
    Column(‘id’, Integer, primary_key = True),
    Column(‘category_id’, Integer, ForeignKey("rss_category.id")),
    Column(‘title’, String, nullable = False),
    Column(‘link’, String),
    Column(‘description’, String)
)

class Cate(object):
    def __init__(self, title=None):
        self.title = title
       
class Feed(object):
    def __init__(self, title=None, link=None, description=None):
        self.title = title
        self.link = link
        self.description = description
       
    def __repr__(self):
        return "title=%s, link=%s, description=%s" % (self.title, self.link, self.description)

Feed.mapper = mapper(Feed, RssFeed)

m = mapper(Cate, RssCategory, properties = {
                    ‘feeds’ : relation(Feed.mapper, lazy=True, private=True)
                }
              )
           
c = m.select()[0]
print c.feeds[0]
print c.feeds[1]

前面大部分的东西你应该比较熟悉了。这里在 Feed 类中还定义了 __repr__ 方法,主要的目的是可以通过 print 语句简单地打印出对象的内容。不一样的地方就是 mapper 的定义。在前面的测试中,通过一个 mapper 就完成了。而这里则是通过两个 mapper 来完成。第一个是实现了 Feed 与 RssFeed 的映射,结果 mapper 保存在 Feed 类本身。其实这个 mapper 放哪里都无所谓的。第二个 mapper 是实现了一对多的关系。让我们比较一下前一个列子就清楚了:

m = mapper(Cate, RssCategory, properties = {
                    ‘feeds’ : relation(Feed, RssFeed)
                }
              )

在这里 relation 中还是类与 Table metadata 的映射。而上面则是一个 mapper 对象,再加上 lazy 和 private 参数。可见 SQLAlchemy 的 mapping 的处理很灵活。两种方法都是可以的,我是这样认为的。再往后则是访问数据的代码。

这里在文档中不知道是不是错误,它使用了一个叫 Mapper 的东西,可是我没有找着。

再往下我看到 ono to one 和 many to many ,但除了发现 many to many 有一个 mapping 的relation使用了一个 mapper 和 一个包含了两个表的主键外,没有看出什么特别的。而且从提供的访问也只是看出一对多的方式来。

这块内容太不详细了,因此留给有兴趣的人继续吧。

在高级 Data Mapping 中还有许多让人感兴存趣的东西,如:

  • 带条件和自定义关联字段的 mapping
  • 设置返回条数和起始位置
  • 用一个类来对应多个表
  • 等等

这些内容有些简单,有些复杂,留给有兴趣的人吧。

2005年12月15日

邮件列表中我连发了数封信报告了我发现的错误,并给出了我认为的解决方案,但无人响应。看一看订阅的人数不到100,而且在邮件列表上的日期是半夜……

对象关系

说正事吧。前面我们已经了解了基本的数据映谢或ORM的一些东西,知道了一些基本的操作。但这远远是不够的。许多ORM模块还提供对象与对象间的关系的实现,象 OneToOne, ManyToOne, ManyToMany。其实不用这些东西也完全没有问题,这就是一个多表查询的问题,使用SQL直接操作是非常简单而且直观。在 SQLAlchemy 的文档中,在讲述 Data Mapping 之前就有许多这样的例子,如果你喜欢这种方式,不妨看一看。但现在我们还是学一学对象间的关系的实现吧,管它有用没用,先学着再说。其实很简单的原因:就是为了方便。不过学起来却不一定轻松,还会带来一些新问题。

首先修改一下 Table Metadata 为:

RssCategory = Table(‘rss_category’, sqlite_engine,
    Column(‘id’, Integer, primary_key = True),
    Column(‘title’, String, nullable = False)
)

RssFeed = Table(‘rss_feed’, sqlite_engine,
    Column(‘id’, Integer, primary_key = True),
    Column(‘category_id’, Integer, ForeignKey("rss_category.id")),
    Column(‘title’, String, nullable = False),
    Column(‘link’, String),
    Column(‘description’, String)
)

注意 RssFeed 是一个新表,因此你应该要创建一下,而 RssCategory 已经创建好了。这里很重要的一点是外键的定义:ForeignKey("rss_category.id")。这是 SQLAlchemy 的缺省关联对象要使用的。当然也可以不指定外键,而显式地指明关联的字段。同时要注意,外键中的参数是真正的表名和字段,而不是Table对象。

既然是学习 ORM ,则必然要定义两个对应的类,为了创建方便,增加相应的 __init__方法。

class Cate(object):
    def __init__(self, title=None):
        self.title = title
       
class Feed(object):
    def __init__(self, title=None, link=None, description=None):
        self.title = title
        self.link = link
        self.description = description

这里都忽略了主键和外键的生成,这些事情由 SQLAlchemy 关心即可。

Table Metadata 和 user-defined class 都有了,mapper如何定义呢?使用下面的方法定义:

m = mapper(Cate, RssCategory, properties = {
                    ‘feeds’ : relation(Feed, RssFeed)
                }
              )

这里多了一个 properties 参数,它是一个字典值。我增加了一个 feeds 值,它又是一个 relation 对象,这个对象实现了 Feed 类与RssFeed的映射。这样我通过RssCategory的feeds属性就可以反映出RssFeed的值来。

这里还要说明,我报告的错误之所以会产生主要是因为在后面的测试中我通过reflect来实现 Table Metadata 的创建,而不是真正我定义的代码。因此上还是有一些区别。特别是在测试上面映射代码时,报了一个错,说是找不到外键的关系。而我查看了生成表的 SQL 语句,其中对于外键的定义是使用REFERENCE这个名字,并且在使用 reflect 得到 Table Metadata 时,根本就没有外键信息。看到以后还是使用 Table Metadata 的好,使用reflect可能就会有问题。

关键定义好了,让我们按文档那样插入数据试试吧:

c = Cate(‘New Category’)
c.feeds.append(Feed(‘One’, ‘http://one.com’, ‘descript one’))
c.feeds.append(Feed(‘Two’, ‘http://two.com’, ‘descript two’))

objectstore.commit()

成功了。下面再取出数据看一看结果:

for i in m.select():
    print i.id, i.title
    for j in i.feeds:
        print j.id, j.title, j.link, j.description

结果是:

1 New Category
1 One http://one.com descript one
2 Two http://two.com descript two

成功,没有问题。

使用关系就是方便,可以从一个对象直接找到相对应的其它的对象,而SQL的方式虽然直观,但不够面向对象,各有所长。

在文档中还举了一个del u.addresses[1]的例子。从这个例子可以看出,删除u.addresses[1]并不是真正将对应的 addresses 表记录删除,它只是将 addresses 相应的记录的外键字段 user_id 置为了 None。这样就表明这条记录不再与任何 User 记录有关系。那么这条记录虽然存在,但是已经没有什么用了。为了避免这个无用的数据,可以在进行Mapping时再加入 private=True 参数,如:

m = mapper(Cate, RssCategory, properties = {
                    ‘feeds’ : relation(Feed, RssFeed, private=True)
                }
              )

这里与 SQLObject 有所不同。在 SQLObject 中关系是一个单独的表,它保存着有关系的表的id对照。这样象我的例子,有两个表,再加上关系表,实际上在数据库中是三个表。而这里仍然是两个表。那么对照表就是由 RssFeed 本身来实现的,因为它多了一个外键字段,而这个字段是存在于 RssFeed 表中的。这样RssFeed 表即有自身的id还有RssCategory的id,因此起到了和 SQLObject 的关系表一样的作用。(这样的话表是少了,但如果关系复杂,会不会照成表字段非常多呢?很乱呢?)

在上面指定了 private=True 之后,它并不是简单的不显示没有对应的记录,而是在删除关系时自动会将关联的记录删除。

Data Mapping

学了半天了,终于要进入大家都感兴趣的 ORM 部分了。在 SQLAlchemy 中叫 Data Mapping,那么在处理时将使用一个叫 Mapper  的对象。关于Data Mapping 的内容,在文档中的详细的描述。当然与一般的 ORM 是相似的。比如一个类的实例对应表中的一条记录,实例的属性对应字段,还提供了一个select()方法可以返回多个对象,它并不需要execute()。这里要注意,在 SQLAlchemy 中使用 Data Mapping 需要三个东西:Table metadata, the user-defined class, and the Mapper。Table metadata 就是建表后的对象。用户定义类很简单,最简单的就是:

class User(object):pass

这与 SQLObject 和 django 都不相同。因为 Table Metadata 已经完成了数据 model 的定义,因此用户定义的类就只需要一个架子了。当然并不都是这样简单的,这个 user-defined class 还可以有象 django 中的 inner class META 和 SQLObject 中的 sqlmeta 的功能,但是直接在这个类本身来实现,不再需要 inner class了(目前是这样理解的)。

还是先看一下例子吧。

>>> class Cate(object):pass
>>> catemapper = mapper(Cate, rss)
>>> r = catemapper.select(rss.c.id == 2)
>>> r[0].id
2
>>> r[0].title
u’UPDATE’

首先创建了一个空类。然后创建了一个mapper对象。使用mapper对象的select()方法来得到对象列表,注意是一个列表。然后返回第一条记录的id和title。

>>> type(r[0])
<class ‘__main__.Cate’>

看到了,每个对象就是一个Cate的实例。这样我理解mapper的作用:

  1. 查询结果
  2. 与类对应并进行相应的映射处理

而 SQLObject 和 django 则是通过 model 类本身所带的 select() 或 get_list() 之类的方法,没有额外的 mapping 处理和对象。

修改下对象属性:

>>> r[0].title = ‘Changed’
>>> objectstore.commit()

修改属性就会返映到数据库中,但要注意,所有的修改都需要执行 objectstore.commit() 。不过这里我发现一个bug,那就是再运行会报错,好象是由于字段取出来是unicode编码造成的,我已经报告,还不知道结果。

最终我发现就是在从 sqlite2 中取表结构时,Column 对象的 name 和 key 都变成unicode了,因为 sqlite2 中就是存的unicode。于是我做了修改:修改schema.py中的Column的__init__,增加将name转为str的处理。这样就解决了问题。代码片段为:

class Column(SchemaItem):
    """represents a column in a database table."""
    def __init__(self, name, type, *args, **kwargs):
        name = str(name)
        self.name = name
        self.type = type
        self.args = args

红色代码是我加的。

在 SQLObject 和 django 中,我们经常会通过对象的生成来向数据库中插入记录,那么在 SQLAlchemy 不能直接这样做,需要一些额外的工作。因为我们定义的类没有构造函数,因此让我们定义一个吧。

class Cate(object):
    def __init__(self, title=None):
        self.id = None
        self.title = title

很简单,就是一堆赋值。那么这里因为id是主键,因此缺省赋为None,这样 SQLAlchemy 就可以自动处理主键了。那么创建Cate的对象就可以向表中添加新记录,如:

catemapper = mapper(Cate, rss)
a = Cate(title="New Title")
objectstore.commit()

这里不要忘了 objectstore.commit() 调用。为什么可以添加数据呢?全是因为 mapper() 的功劳。一旦一个 user-defined class 与 Table metadata 关联,那么一旦创建对象,它就可以与添加记录相关联。注意,这个类的__init__方法的所有参数都应该有缺省值,这样不用参数也可以创建对象。 SQLAlchemy 需要这样,特别是在使用 mapper 的 select() 方法来得到对象列表时。

仔细看一看 objectstore.commit() ,它其实还有其它的参数,commit()可以传入一个对象集,表示想要提交哪些变动的对象。每次对一个对象做修改, SQLAlchemy 会记录下对象的修改情况。如果在commit()中没有指定对象,则会保存上次调用 objectstore.begin()  之后的所有变化的对象。原来这里来藏着不少东西。特别是事务的处理。

上面讲了很多有些乱,总结一下:

  • 实现一个Data Mapping 需要Table Metadata, user-defined class, mapper
  • 选择多条记录,可以使用mapper对象的select()方法,注意返回的是一个对象list。
  • 插入记录可以先创建空对象,然后设定属性,再执行objectstore.commit()。这种情况是当类很简单时可以这样。如果类定义了 __init__方法,则可以一次在创建实例时完成,仍然要注意调用 objectstore.commit()。

我喜欢这种可以自由控制变化是否提交的方式,当然是有些麻烦。象 Django 创建一个新的对象要执行save()方法,而 SQLObject 一般不用。

查询处理

查询使用select()来处理,但内容要复杂得多。

让我先熟悉一下最基本的,毕竟目前就一个表,操作应该是非常的简单。让我继续用 rss 对象来处理。

>>> r = rss.select().execute()

还记得我写过sql的执行过程,先是生成一个 sql 语句对象,然后再去执行。这里r是一个DBAPI的cursor,你需要使用fetchone()或fetchall()来得到返回值。这些真都是传统的东西。

>>> r.fetchall()
[(1, u'TEST'), (2, u'\u4e2d\u6587')]

如果想重复r是不可能的,因为DBAPI返回的r应该是一个generator,一旦处理完毕是不可重用的。不过一般也是这样,只有做测试才会想到重用。改一下:

>>> rs = rss.select()
>>> r = rs.execute()
>>> rol = r.fetchone()

这样rol就是第一条记录了。可以使用rol[0], rol[1]来访问,还可以使用rol['id'], rol['title']来访问:

>>> rol['id']
1
>>> rol['title']
u’TEST’

除了使用 table.select() 这样的形式, SQLAlchemy 还直接提供了一些顶层方法来方便处理查询,如 select() 方法:

>>> r = select([rss]).execute()
>>> r.fetchall()
[(1, u'TEST'), (2, u'\u4e2d\u6587')]

不想陷在select中过多,这部分东西需要多看,多练。先跳过去吧。下面看一看如何删除数据吧。

删除数据

使用上面的 rss。

>>> rss.delete(rss.c.id == 1).execute()
>>> r = select([rss]).execute()
>>> r.fetchall()
[(2, u'\u4e2d\u6587')]

发现在 NewEdit 中执行多次后可能会出现无法回滚的异常,不知道是怎么回事,只好关了 NewEdit 重启就好了。

更新数据

和insert()差不多。

>>> rss.update(rss.c.id == 2).execute(title=’UPDATE’)
>>> r = select([rss]).execute()
>>> r.fetchall()
[(2, u'UPDATE')]

关于数据库引擎,在 SQLObject 中也要创建类似的东西,不过它可以通过一个统一的 sqlhub.processConnection = connection 来实现所有 Model 使用同一个数据库的连接,这样的确很方便。但由于在使用线程中出现了问题,因此我只好每个 Model 传入一个连接。目前在 SQLAlchemy 中还没有测试到线程,不过可以从 Table 的创建过程中看到它需要一个显示的数据库引擎对象,这一点与 SQLObject 有所区别。

 在前面我们创建了表对象之后,就可以使用属性方式来访问表的信息。如表的字段,可以使用 table.c 或 table.columns,可以了解字段的主键信息等,在 SQLAlchemy 文档中有详细的描述。注意,这是用来看表结构的,还没有到看记录的时候。

MetaData与Data Mapping

SQLAlchemy 很有特点的就是提供两种访问数据的方式,一种是基于 DBAPI 的传统方式,另一种是独立的 Data Mapping 的方式。Data Mapping 方式实现了象 SQLObject 和 django 一样的 ORM 功能。因此可以满足不同人的口味。那么我先学一下传统的方式吧。

插入数据

因为没有数据,所以第一步是要插入数据。例子就按前面创建的 RssCategory 表来进行。我测试时是这样的:将建表的语句放在了一个 Python 文件中,然后一执行即可:

from sqlalchemy import *
sqlite_engine = create_engine(’sqlite://filename=d:/test.db’, echo=True)

RssCategory = Table(‘rss_category’, sqlite_engine,
    Column(‘id’, Integer, primary_key = True),
    Column(‘title’, String, nullable = False)
)
RssCategory.create()

可以看到,只有两个字段。id是主键。title是字段不能为空。

在测试时我就不需要这个文件了,因为 SQLAlchemy 提供了 reflect 功能,因此我可以方便地从 Python Shell 中得到这个表对象,象这样:

>>> from sqlalchemy import *
>>> sqlite_engine = create_engine(’sqlite://filename=d:/test.db’, echo=True)
>>> rss = Table(‘rss_category’, sqlite_engine, autoload=True)

这里我加入了echo参数,这样执行的 SQL 语句就可以看到了。

下面插入数据。在文档中有许多种处理方式。基本的 SQL 处理是这样的:

  1. 调用不同的SQL方法,如select(), insert(), update(), delete(),这样得到的是一个对象。在调用时可以指定一些参数,具体要看不同的操作语句。对于得到的对象,如果使用str()来处理,可以得到相应的 SQL 语句

    >>> str(rss.insert())
    ‘INSERT INTO rss_category (id, title) VALUES (:id, :title)’
  2. 对于得到的对象再调用它的execute()方法来执行,同时也可以传入一些参数。
  3. 可以把处理联起来调用,如:

    rss.insert().execute()

在参数的处理上,SQLAlchemy 表现得非常灵活,从文档中的例子就可以看出。我照抄过来算了,与我的例子不同:

# basic insert
users.insert().execute(user_id=1, user_name=’jack’, password=’asdfdaf’)

# insert just user_name, NULL for others
# will auto-populate primary key columns if they are configured
# to do so
users.insert().execute(user_name=’ed’)

# INSERT with a list: 
users.insert(values=(3, ‘jane’, ’sdfadfas’)).execute()


# INSERT with user-defined bind parameters 
i = users.insert(
values={’user_name’:bindparam(‘name’), ‘password’:bindparam(‘pw’)}

i.execute(name=’mary’, pw=’adas5fs’)


# INSERT many – if no explicit ‘values’ parameter is sent,
# the first parameter list in the list determines
# the generated SQL of the insert (i.e. what columns are present)
# executemany() is used at the DBAPI level
users.insert().execute(
{’user_id’:7, ‘user_name’:'jack’, ‘password’:'asdfasdf’}
{’user_id’:8, ‘user_name’:'ed’, ‘password’:'asdffcadf’}
{’user_id’:9, ‘user_name’:'fred’, ‘password’:'asttf’}
)

第一个例子是全有字段在 execute 中给出,并且是关键字参数的形式。

第二个例子只给出了不能为空的字段,其它的,象主键会自动创建,剩下的就为空了。(突然想到,有没有缺省值的设置呢?目前还没有看到)

第三个例子是在insert()中指定values参数来提供数据,这种方式与上面就不一样了。并且是一个tuple的形式。

第四个例子是在insert()中提供了字段绑定参数,真正的值在execute()方法中给出。又是一种变化。同时可以看到,sql语句与执行操作是分离的。相当于许多数据库提供的 PREPARE 功能,SQL 预编译,可以提高执行速度。

第五个例子是在execute()中提供多个字典参数,实现一次插入多条记录的目的。

真的是很灵活,当然这并不是对象方式的。下面我向 RssCategory 中插入几条数据。

>>> rss.insert().execute(title="TEST")
>>> rss.insert().execute(title=unicode(‘中文’, ‘gbk’))
>>> rss.insert().execute(id=1, title=’Another’)
OperationalError: PRIMARY KEY must be unique

第一句没有给出主键来,先插入了一条"TEST"记录。其实还有调试结果没有贴出来。第二句是插入了中文,但使用了Unicode。我发现查出的结果是Unicode,而pysqlite2只使用Unicode,因此转为Unicode的话,处理中文没有问题。第三句指定了id,但发现重复了。因此象主键,如果不给出自动生成,但一旦给出就不能重复。

插入完毕,下面就可以学习select()了。

注意上面的大括号全部转为了全角了,不能直接拷贝使用。

2005年12月14日

为什么要学

管它呢?先学着。感觉 SQLObject 在 NewEdit 中的 RssReader 导入太慢,自已重写又嫌麻烦。正好看到 xlp223 推荐了这个,先学着再说。第一印象:很有特色。不过用得人较少。订阅了它的邮件列表也没见有许多人发信。

学习没有太明确的目标,反正是希望替换掉整个 RssReader 的东西。很可能以它为实现的目标。

SQLAlchemy 支持多种数据库,有SQLite, Postgres, MySQL, and Oracle,看到它的许多例子都以 Sqlite 为例,我很高兴,因为我喜欢使用 Sqlite 这个小型的数据库。我想它对 sqlite 的支持应该很好。以后的学习都以 sqlite 为主。

创建一个数据引擎

from SQLAlchemy import *
sqlite_engine = create_engine(’sqlite://filename=d:/test.db’)

这是使用了近似协议的描述方式。还可以传入字典方式:

from sqlalchemy import *
sqlite_engine = create_engine(’sqlite’, {’filename’:'d:/test.db’})

这是最简单的。还可以带参数:

echo=True 表示输出调试结果,缺省为stdout
logger为一个类文件对象(不是一个文件名),当echo=True时用来输出。缺省为stdout

还有其它几个参数,暂时用不上就不管它了。

然后就是表结构的描述了。

使用MetaData进行描述表结构

这里采用的不是象 SQLObject 和 django 一样的类方式描述,而是更接近于 Create table 语句,不过,这种方式我更习惯。

对于新建的表可以自行定义,并且 SQLAlchemy 提供了相应的create()和drop()方法。但不象 SQLObject 一样可以自动判断表是否存在(目前我在 SQLAlchemy 还没有发现)。同时也提供同其它一样的自动从已经存在的数据库创建相应的 MetaData 对象。

建表就象写一个 SQL 语句,如以 Rss Reader 为例:

RssCategory = Table(‘rss_category’, sqlite_engine,
    Column(‘id’, Integer, primary_key = True),
    Column(‘title’, String, nullable = False)
)

主个很简单,但是我也发现有趣的东西。在 SQLAlchemy 的文档中,象String类型一般都有长度,写为String(10)之类的。那么如果我想不要长度呢?就使用上面的 String 就行了。而相应的源码我查了一下, String 是一个类,当长度为空时,最终会返回一个 TEXT() 的实例。因此它会根据有没有长度自动选择合适的对象。不过这又引出一个问题,Integer是类,而 String(10) 则是实例,不知道 SQLAlchemy 是如何区分的。不过,我也懒得去细看了,反正知道这里可以用类的形式,也可以用实例的形式,最终都会是实例的形式。做这样的判断倒也并不困难。

整个结构看上去就象是 Create 语句,先是一个表名(要指定),然后是一个引擎对象,后面是字段定义。与 django 或 SQLObject 不同,这里没有什么 magic 的处理方法,一些都显得原始。不过也少了许多的猜测。字段定义可以带一些参数,如 primary_key, nullable这两个都容易明白。还有key表示可以起一个别名。

然后调用 table 对象的 create() 来创建表。

RssCategory.create()

这时,如果你在 sqlite_engine 创建时,加入了前面所写的 echo=True 参数,你会看到有 SQL 语句输出:

CREATE TABLE rss_category(
 id INTEGER NOT NULL PRIMARY KEY,
 title TEXT NOT NULL
)

看到了吧,String变成了 TEXT 了。

一旦建好表,就可以非常方便地访问表了。不过现在表里没有数据呢。如果想删除则执行表对象的 drop() 方法即可。不过不知道如何自动判断一个表是否已经存在。

如果一个表已经存在,那么 SQLAlchemy 还可以自动反射(reflect)出来。

rss = Table(‘rss_category’, sqlite_engine, autoload=True)

然后可以使用 rss.c.keys() 看到所有的字段名 ['id', 'title']