什么是 SQLAlchemy ?

The Python SQL Toolkit and Object Relational Mapper

create engine

首先 要创建 Engine 实例

sqlalchemy.create_engine(*args, **kwargs)

创建 mysql 连接

# driver mysql-python
DB_PATH = "mysql+mysqldb://root:password@host:port/dbname?charset=utf8mb4"
xchat_engine = create_engine(
    DB_PATH,
    echo =False,
    pool_size=40,
    pool_recycle=3600
)

连接的字符串 URL 格式 dialect[+driver]://user:password@host/dbname[?key=value..] ,其中

  • dialect 是使用的数据库 mysql, oracle, postgresql 等等
  • driver 是连接数据库需要的驱动 DBAPI,比如 psycopg2, pyodbc, cx_oracle 等等

另外这个字符串也可以使用 sqlalchemy.engine.url.URL 实例。

对于 MySQL 而言,driver 可以有如下选择

# default
engine = create_engine('mysql://scott:tiger@localhost/foo')

# mysql-python
engine = create_engine('mysql+mysqldb://scott:tiger@localhost/foo')
# sudo apt-get install libmysqlclient-dev
# pip install mysqlclient

# MySQL-connector-python
engine = create_engine('mysql+mysqlconnector://scott:tiger@localhost/foo')

# OurSQL
engine = create_engine('mysql+oursql://scott:tiger@localhost/foo')

# pymysql
engine = create_engine('mysql+pymysql://user:pass@localhost/foo')

PyMySQL 是使用纯 Python 实现的。

Declare a Mapping

定义和数据库 Map 的实体

from sqlalchemy import Column, Integer, String
from sqlalchemy.ext.declarative import declarative_base

Base = declarative_base()

class SomeClass(Base):
    __tablename__ = 'some_table'
    id = Column(Integer, primary_key=True)
    name =  Column(String(50))

declarative_base() 方法会返回一个 base class,所有自己定义的 class 都要从此继承。关于定义类(表)中数据类型的选择,可以参考文末的对应关系。定义好之后 sqlalchemy.schema.Tablesqlalchemy.orm.mapper 会产生。结果可以从类变量获取

# access the mapped Table
SomeClass.__table__

# access the Mapper
SomeClass.__mapper__

如果想要自己定义的字段和数据库中字段相区别,可以在 Column 中定义 some_table_id

id = Column("some_table_id", Integer, primary_key=True)

通过 base class 定义的类包含一个 MetaData 通过这个 MetaData 可以定义 Table ,然后通过如下语句就可以直接在数据库中生成定义好的表。

engine = create_engine('sqlite://')
Base.metadata.create_all(engine)

创建 Map 类实例

加入定义了 User 类,创造一个实例

ed_user = User(name='ed', fullname='Ed Jones', password='edspassword')

创建 Session

做好了一切准备,可以和数据库进行交互,ORM 和数据库打交道的是 Session ,从之前定义的 engine 中创建 sessionmake

from sqlalchemy.orm import sessionmaker
Session = sessionmaker(bind=engine)

这个 Session 实例,是一个 session 创造的工厂,之后所有的 session 都可以由他创建

session = Session()

这里创建的 session 实例还不会开启任何连接,当第一次被用到,session 会从 Engine 维护的连接池中获得一个连接,一直持有该连接知道 commit 所有的提交或者显式的关闭 session 对象。

Session 的生命周期模式,记住,Session 只是特定对象的一个工作区,限定到特定的数据库连接,

关于 Session 使用更多的问题 可以参考之前的文章。当然官网也有很多使用说明

Adding and Updating Objects

当有了 Session 之后,就可以通过 session 来操作数据库。比如

session.add(ed_user)

当把实例对象 add 到 session 之后,其实不会直接提交给数据库,这个时候只有 flush 操作,这个时候

our_user = session.query(User).filter_by(name='ed').first()

也能够将刚刚 add 的实例检索出来,真正的提交只有执行 session.commit() 之后。

如果发现单一插入数据库比较费时,那么在 SQLAlchemy 1.0.0 之后,可以使用新引入的特性批量操作。

在 Session 中有

  • Session.bulk_save_objects()
  • Session.bulk_insert_mappings()
  • and Session.bulk_update_mappings()

简单的例子:

s = Session()
objects = [
    User(name="u1"),
    User(name="u2"),
    User(name="u3")
]
s.bulk_save_objects(objects)
s.commit()

详情见官网 bulk operations

Querying

查询

# User objects
for instance in session.query(User).order_by(User.id):
    print(instance.name, instance.fullname)

# tuples
for name, fullname in session.query(User.name, User.fullname):
    print(name, fullname)

# named tuples, supplied by the KeyedTuple class can be treated much like an ordinary Python object
for row in session.query(User, User.name).all():
    print(row.User, row.name)
> <User(name='wendy', fullname='Wendy Williams', password='foobar')> wendy

# 控制返回字段
for row in session.query(User.name.label('name_label')).all():
    print(row.name_label)

from sqlalchemy.orm import aliased
user_alias = aliased(User, name='user_alias')
for row in session.query(user_alias, user_alias.name).all():
    print(row.user_alias)

for u in session.query(User).order_by(User.id)[1:3]:
    print(u)

for name, in session.query(User.name).\
    filter_by(fullname='Ed Jones'):
    print(name)

for name, in session.query(User.name).\
    filter(User.fullname=='Ed Jones'):
    print(name)

其他跟多的 filter 操作可以参考这里

Returning Lists and Scalars

在查询时可以通过这些方法来选择返回的数量

query = session.query(User).filter(User.name.like('%ed')).order_by(User.id)
query.all()         # return list
query.first()       # return first
query.one()         # multiple rows found raise an MultipleResultsFound, or no row found raise NoResultFound
query.one_or_none() #  if no results are found, it doesn’t raise an error just return None, however, it does raise an error if multiple results are found.
query.scalar()      # 调用 one() 方法,一旦成功,返回行的第一列

SQLAlchemy 同样也支持直接使用 Text 来写 sql 语句,具体可以参考官网

计数可以使用 count

session.query(User).filter(User.name.like('%ed')).count()

from sqlalchemy import func
session.query(func.count(User.name), User.name).group_by(User.name).all()

session.query(func.count(User.id)).scalar()

更多关于外键等等,可以查看官网:http://docs.sqlalchemy.org/en/latest/orm/tutorial.html

SQLAlchemy vs Python vs SQL 数据结构对应表

SQLAlchemy Python SQL
BigInteger int BIGINT
Integer int INTEGER
Boolean bool BOOLEAN or SMALLINT
Date datetime.date DATE (SQLite: STRING )
DateTime datetime.datetime DATETIME (SQLite: STRING )
Enum str ENUM or VARCHAR
Float float or Decimal FLOAT or REAL
Interval datetime.timedelta INTERVAL or DATE from epoch
LargeBinary byte BLOB or BYTEA
Numeric decimal.Decimal NUMERIC or DECIMAL
Unicode unicode UNICODE or VARCHAR
Text str CLOB or TEXT
Time datetime.time DATETIME

其他重要内容

在定义 Model 时定义表引擎和编码

在 model 中使用 __table_args__ 来定义

class User(BaseModel):
    __tablename__ = 'users'
    __table_args__ = {
        "mysql_engine": "InnoDB",    # 引擎
        "mysql_charset": "utf8"         # 编码
    }

    ...

模型属性名和表字段名不一样

在定义 Column 时指定

id = Column('id', Integer, primary_key=True, autoincrement=True)

定义多列组合唯一索引

有些时候定义表时需要多列组合唯一,比如

class User(Base):
    field1 = Column(Integer, primary_key=True)
    field2 = Column(Integer, primary_key=True)

那么可以

class User(Base):
    field1 = Column(Integer)
    field2 = Column(Integer)
    __table_args__ = (
        UniqueConstraint('field2', 'field1'),
        {},
    )

From: StackOverflow

编码问题

在使用 mysqldb 这个 driver 的时候莫名其妙遇到编码的问题,数据库,表,都是 utf8mb4,在 CentOS6 下面死活用不了 utf8mb4 作为连接编码

can't initialize character set utf8mb4 (path /usr/share/mysql/charsets/)

一直都是这个错误,但在我本地一切 OK,这时候我想是不是机器 driver 的问题,换成 pymysql 之后就 OK 了。

http://docs.sqlalchemy.org/en/latest/dialects/mysql.html#charset-selection

也是醉了。

数据库表没有主键

SQLAlchemy 在映射数据库时会寻找数据的 Primary Key,如果没有会报错。1

sqlalchemy.exc.ArgumentError: Mapper mapped class PicSearchword->pic_searchword could not assemble any primary key columns for mapped table 'pic_searchword'

SQLAlchemy ORM 会必须要求表拥有 Primary Key,而如果代码是建立在 Alchemy Core 之上,使用 Table 和 select() 则没有这个要求。

reference

  • 《Essential SQLAlchemy 2nd Edition 2015》