python数据库查询--开源项目Records源码分析

零:说明

Records是一个对python中关系型数据库查询的封装,简化了在python中进行数据库查询的操作。项目地址如下:GitHub – kennethreitz/records: SQL for Humans™

作者Kennethreitz是requests和python-guide 额作者,python领域的大牛人物之一,关于他还有一个励志的故事:谁说程序员不是潜力股?让这位世界前五名的天才程序员来颠覆你三观!

项目本身代码短小,总共只有一个文件不超过500行,如果是第一次尝试阅读python开源项目,这是一个很好的选择。正好最近在学习数据库方面的相关知识,也可以加深一下对关系型数据库的理解。

Records实现了对直接输入SQL语句(raw SQL)进行查询的返回结果的优化,并没有实现ORM之类高级的技巧,这种注重使用体验的优化,正如其名:SQL for humans。尽管如此,作者所使用的架构技巧,包括惰性查询,缓存的使用,还是有着一定的借鉴意义。

一:项目分析

这个项目中的代码并不是很多,因此打算将项目里的每个类都分析一下。虽然逻辑并不复杂,但是作者对每个类的设计和架构,对python中类相关技巧都使用,都有一些可以学习的地方。

  • Record

这是用来存储查询结果(也就是数据库中的一行)的类,用了很多类的特殊方法进行架构,因此重点分析这些特殊方法。

python提供特殊命名的方法来拦截运算,以双下划线(__X__)命名的钩子与内置运算一一对应。

部分特殊方法的参考文档

  1. __slots__

    __slots__包含类属性的字符串,可以限制类的合法属性集,只对新式类有作用。是为了节省实例的内存消耗,类中的方法不受其限制。新式类中原本含有一个__dict__属性,用来记录实例中所有的属性和方法,也是通过这个字典,可以让实例绑定任意的属性,而__slots__存在时,就不会有__dict__变量。
    __slots__定义的属性仅对当前类起作用,对继承的子类是不起作用的,除非在子类中也定义__slots__,这样,子类允许定义的属性就是自身的__slots__加上父类的__slots__

  2. __getitem__()

__getitem__()是对[]的重载,同时,函数内使用了index()对列表中的内容反查其索引,实现了通过record[key]查找value的功能。如果是我大概会用一个索引迭代,但实际上不需要。

  1. __dir__()

    __dir__()方法指明了对实例使用内置函数dir()时需要返回的列表, 源代码中在基类的基础上加上了keys的值列表。

  2. 其他

    对于类实例的显示用了另一个他自己写的库tablib,用于对数据进行格式化。

    在as_dict()中用到了一个三目运算符

    A = X if conditon else Y 
    #等价于:
    if conditon:
        A = X
    else:_all_rows
        A = Y
    
  • RecordCollection

这个类是Record的集合类,用于存储查询(query)结果。

  1. 构造

    构造函数传入一个逐行的Record generator,存储在成员变量_rows 中。

    generator支持迭代,通过圆括号形成的生成器表达式生成,例如:

    (x for x in range(5)) # <generator object <genexpr> at 0x7fd55f3fe9b0>

  2. __iter__() & yield & next()

    __iter__()next()与迭代器有关,在Python中实现了__iter__()方法的对象是可迭代的,实现了next()方法的对象是迭代器(iterator)
    for循环为例,事实上,for i in obj的迭代就是通过内置函数iter(obj)调用它的__iter__()方法取得一个迭代器对象(但在源码中是返回了一个生成器,见下文说明)。

    源码中在__iter__()函数内部使用了yeild关键字。含有yield的函数称为生成器函数,生成器函数返回时使用yield关键字,与return的用法类似,但是,生成器函数返回时会保存包含其本地作用域的状态,之后将函数挂起,在下次调用函数时,从挂起的地方继续。事实上,含有yield的语句被调用时会产生一个生成器(generator)从而支持迭代。

    生成器和迭代器都通过next()方法不断生成下一个对象,如果迭代到达末尾,在next() 中会抛出StopIteration异常。

    <u>generator自身是一个iterator,但只支持一次迭代。</u>

  3. 缓存

    _all_rows变量用于缓存,缓存有几种不同的情况需要考虑:

    • 顺序迭代,在next()中一旦执行了_rows_.next()就缓存迭代结果。
    • 直接调用collection[index]操作获取指定行,在__getitem__()中会顺序迭代到指定位置,对在指定行之前的行全部缓存,同时,之后如果再调用iterator,就不需要调用iterator.next()而是直接从缓存中取用,这一点在__iter__()中也有体现。
    • 要显示对象本身的时候,会显示其中包含的行,此时要求对_rows 迭代求值,并且缓存。在源代码中,位于all()函数中,通过rows = list(self)调用类的__iter__()方法。
  • Database

这是执行数据库查询与数据库连接的类。

  1. SQLAlchemy

    数据库查询主要依赖于SQLAlchemy库的调用,这是一个python的ORM框架。ORM即对象关系映射,简而言之,就是把数据库的一个个table(表),映射为编程语言的class(类),这里主要介绍在源码中使用到的几个方法。SQLAlchemy实际上可以将数据库查询完全转化为对象的调用,但是在records这个库里,只是对原生的数据库查询返回进行了封装,所有并没有使用到这些功能。

    详细使用方法:SQLAlchemy 1.1 Documentation

    • create_engine()方法进行数据库连接,返回一个database对象。之后通过engine.connect()获取connection, 然后通过其执行sql, 叫做connection执行,其实通过engine.execute()也可以执行sql,不过connection执行与使用transaction模式有关, 如果不涉及transaction, 两种方法效果是一样的。

    • inspect()方法创建了一个Inspector对象。通过这个对象,可以实现对数据库中的schematableconstraint等结构的查询。例如,insp(engine).get_table_names()

    • connection.excute("sql")返回一个p = ResultProxy对象,通过p.keys()可以得到查询的列名,而通过迭代p可以获得查询的每一行,以此建立一个RecordCollection

    • text()用于提供附带参数的查询功能,例如:

      t = text("SELECT * FROM users WHERE id=:user_id")
      result = connection.execute(t, user_id=12)
      
  2. 上下文管理器

    有一些任务,可能事先需要设置,事后做清理工作。对于这种场景,Python提供了一些解决的方案(以文件读写为例)。

    • 利用try-finally语句块来进行包装,这种方法易于理解,但是当逻辑复杂时就会十分混乱:

      writer = open(filename, mode)
      try:
          writer.write(“”)
      finally:
          writer.close()
      
    • 通过with语法重写以上逻辑,就可以简化为:

      with open(filename, mode) as writer:
          writer.write(“”) 
      

      事实上,with语句基本思想是with所求值的对象必须有一个__enter__()方法,一个__exit__()方法。紧跟with后面的语句被求值后,返回对象的__enter__()方法被调用,这个方法的返回值将被赋值给as后面的变量。当with后面的代码块全部被执行完之后,将调用前面返回对象的__exit__()方法。

      __exit__()还可以处理异常,在with后面的代码块抛出任何异常时,__exit__()方法被执行。异常抛出时,与之关联的type,value和stack trace传给__exit__(self, exc_type, exc_val, exc_stack)方法。因此,可以在清理资源,关闭文件等等操作后,将异常重新抛出。

  3. query_file

    通过open()操作读取一个文件,然后通过query()函数执行内部sql语句。

  4. transition

    通过connection.begin()方法实现。

    原文作者:Router8008
    原文地址: https://www.jianshu.com/p/8422cf90b6ae
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞