Python高级技术之:`SQLAlchemy`的声明式(`Declarative`)和经典式(`Classic`)映射。

各位观众老爷们,大家好!今天咱们来聊聊Python ORM框架SQLAlchemy里的两种主要映射方式:声明式(Declarative)和经典式(Classic)。别害怕,这俩家伙虽然听起来像魔法咒语,但其实就是把Python类跟数据库表关联起来的不同方法。咱们争取用最接地气的方式,把它们扒个底朝天,让大家以后写代码的时候,不再迷茫。

开场白:为什么要映射?

在开始之前,咱先得搞清楚一个问题:为什么要映射?想象一下,你写了一个Python程序,需要从数据库里读取数据,或者往数据库里写入数据。如果没有ORM,你就得手写SQL语句,像这样:

import sqlite3

conn = sqlite3.connect('example.db')
cursor = conn.cursor()

# 查询数据
cursor.execute("SELECT * FROM users WHERE id = ?", (1,))
result = cursor.fetchone()
print(result)

# 插入数据
cursor.execute("INSERT INTO users (name, email) VALUES (?, ?)", ('Alice', '[email protected]'))
conn.commit()

conn.close()

手写SQL语句当然可以,但有几个问题:

  • 繁琐: 每一次操作都要拼SQL,代码量巨大。
  • 容易出错: 手残党一不小心就拼错SQL,导致程序崩溃。
  • 不安全: 容易受到SQL注入攻击。
  • 不Pythonic: 看起来不像Python代码,更像SQL代码。

所以,我们需要ORM (Object-Relational Mapping),它就像一个翻译器,把Python对象和数据库表之间的数据进行转换。你只需要操作Python对象,ORM会自动帮你生成SQL语句,执行数据库操作。

SQLAlchemy就是Python世界里最流行的ORM框架之一。它提供了两种主要的映射方式,让我们可以轻松地把Python类映射到数据库表:声明式和经典式。

第一幕:经典式映射(Classic Mapping)

经典式映射是SQLAlchemy的老牌映射方式,也是最底层、最灵活的一种。它允许你完全控制映射过程,但也意味着你需要写更多的代码。

  1. 定义Table对象:

首先,你需要创建一个Table对象,用来描述数据库表的结构。Table对象包含表的名称、列的定义、约束等信息。

from sqlalchemy import create_engine, MetaData, Table, Column, Integer, String, ForeignKey
from sqlalchemy.orm import sessionmaker

engine = create_engine('sqlite:///:memory:') # 使用内存数据库,方便演示
metadata = MetaData()

users_table = Table('users', metadata,
    Column('id', Integer, primary_key=True),
    Column('name', String(50)),
    Column('email', String(50))
)

addresses_table = Table('addresses', metadata,
    Column('id', Integer, primary_key=True),
    Column('email_address', String(50), nullable=False),
    Column('user_id', Integer, ForeignKey('users.id'))
)

metadata.create_all(engine) # 创建表

这段代码定义了两个表:usersaddressesusers表有idnameemail三个列,addresses表有idemail_addressuser_id三个列,其中user_id是外键,指向users表的id列。

  1. 定义Python类:

接下来,你需要定义Python类,用来表示数据库表中的数据。

class User:
    def __init__(self, name, email):
        self.name = name
        self.email = email

    def __repr__(self):
        return f"<User(name='{self.name}', email='{self.email}')>"

class Address:
    def __init__(self, email_address, user_id):
        self.email_address = email_address
        self.user_id = user_id

    def __repr__(self):
        return f"<Address(email_address='{self.email_address}')>"

这两个类分别对应usersaddresses表。

  1. 进行映射:

现在,我们需要使用SQLAlchemymapper()函数,把Python类和Table对象关联起来。

from sqlalchemy.orm import mapper

mapper(User, users_table)
mapper(Address, addresses_table)

mapper()函数把User类映射到users_tableAddress类映射到addresses_table

  1. 使用Session:

最后,我们需要使用SQLAlchemySession对象,来执行数据库操作。

Session = sessionmaker(bind=engine)
session = Session()

# 创建User对象
user1 = User(name='Alice', email='[email protected]')
user2 = User(name='Bob', email='[email protected]')

# 添加到Session
session.add_all([user1, user2])

# 提交事务
session.commit()

# 查询数据
users = session.query(User).all()
print(users)

# 创建Address对象
address1 = Address(email_address='[email protected]', user_id=user1.id)
session.add(address1)
session.commit()

# 查询Address数据
addresses = session.query(Address).all()
print(addresses)

# 关闭Session
session.close()

这段代码创建了一个Session对象,然后创建了两个User对象,把它们添加到Session中,并提交了事务。最后,查询了数据库中的所有User对象,并打印出来。

经典式映射的优缺点:

  • 优点:

    • 灵活: 可以完全控制映射过程,适用于复杂的场景。
    • 底层: 可以更好地理解SQLAlchemy的工作原理。
  • 缺点:

    • 冗长: 需要写更多的代码。
    • 复杂: 学习曲线陡峭,容易出错。

第二幕:声明式映射(Declarative Mapping)

声明式映射是SQLAlchemy推荐的映射方式,它使用一种更简洁、更Pythonic的方式来定义映射关系。

  1. 定义Base:

首先,你需要创建一个Base类,它是所有声明式类的基类。

from sqlalchemy.ext.declarative import declarative_base
Base = declarative_base()
  1. 定义声明式类:

接下来,你需要定义声明式类,用来表示数据库表中的数据。声明式类继承自Base类,并使用__tablename__属性来指定表的名称,使用Column对象来定义列。

from sqlalchemy import Column, Integer, String, ForeignKey
from sqlalchemy.orm import relationship

class User(Base):
    __tablename__ = 'users'

    id = Column(Integer, primary_key=True)
    name = Column(String(50))
    email = Column(String(50))

    addresses = relationship("Address", back_populates="user")

    def __repr__(self):
        return f"<User(name='{self.name}', email='{self.email}')>"

class Address(Base):
    __tablename__ = 'addresses'

    id = Column(Integer, primary_key=True)
    email_address = Column(String(50), nullable=False)
    user_id = Column(Integer, ForeignKey('users.id'))

    user = relationship("User", back_populates="addresses")

    def __repr__(self):
        return f"<Address(email_address='{self.email_address}')>"

这段代码定义了两个声明式类:UserAddressUser类的__tablename__属性指定表名为usersidnameemail是三个列。Address类的__tablename__属性指定表名为addressesidemail_addressuser_id是三个列,其中user_id是外键,指向users表的id列。

relationship 定义了表之间的关系。 在 User 类中,addresses = relationship("Address", back_populates="user") 定义了 User 和 Address 之间的一对多关系(一个用户可以有多个地址)。back_populates="user" 表示在 Address 类中有一个名为 user 的属性,用于反向引用关联的 User 对象。

Address 类中,user = relationship("User", back_populates="addresses") 定义了 Address 和 User 之间的一对一关系(一个地址属于一个用户)。back_populates="addresses" 表示在 User 类中有一个名为 addresses 的属性,用于反向引用关联的 Address 对象。

  1. 创建表:

接下来,你需要使用Base类的metadata.create_all()方法,创建数据库表。

from sqlalchemy import create_engine
engine = create_engine('sqlite:///:memory:')
Base.metadata.create_all(engine)
  1. 使用Session:

最后,我们需要使用SQLAlchemySession对象,来执行数据库操作。

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

# 创建User对象
user1 = User(name='Alice', email='[email protected]')
user2 = User(name='Bob', email='[email protected]')

# 添加到Session
session.add_all([user1, user2])

# 提交事务
session.commit()

# 查询数据
users = session.query(User).all()
print(users)

# 创建Address对象
address1 = Address(email_address='[email protected]', user_id=user1.id)
session.add(address1)
session.commit()

# 查询Address数据
addresses = session.query(Address).all()
print(addresses)

# 关闭Session
session.close()

这段代码和经典式映射的代码基本一样,只是创建UserAddress对象的方式略有不同。

声明式映射的优缺点:

  • 优点:

    • 简洁: 代码更简洁,更易于阅读和维护。
    • Pythonic: 更符合Python的编程风格。
    • 方便: 自动生成表结构,减少了手动编写SQL语句的工作量。
  • 缺点:

    • 灵活性稍差: 对于一些复杂的场景,可能需要使用更底层的API。
    • 抽象程度较高: 可能会隐藏一些SQLAlchemy的细节。

第三幕:总结与对比

特性 经典式映射 声明式映射
代码量 较多 较少
灵活性 较低
学习曲线 陡峭 较平缓
易用性 较低 较高
推荐使用程度 复杂场景或需要完全控制映射过程的情况 大部分场景,尤其是新手入门
定义方式 先定义Table对象,再用mapper()函数映射 定义继承自Base的声明式类,自动生成表结构

经典式 vs 声明式: 选哪个?

说了这么多,那么问题来了,我们应该选择哪种映射方式呢?

  • 如果你是新手,或者项目比较简单,建议使用声明式映射。 声明式映射更易于学习和使用,可以快速上手。
  • 如果你需要完全控制映射过程,或者项目非常复杂,可以考虑使用经典式映射。 经典式映射提供了更高的灵活性,可以满足一些特殊的需求。
  • 如果项目已经使用了经典式映射,并且运行良好,没有必要迁移到声明式映射。

第四幕:一些高级用法

  1. 混合式属性 (Hybrid Attributes):

混合式属性允许你在类级别定义一个属性,它既可以像普通Python属性一样访问,也可以在SQLAlchemy查询中使用。这对于定义一些复杂的计算属性非常有用。

from sqlalchemy import Column, Integer, String
from sqlalchemy.ext.hybrid import hybrid_property
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker

Base = declarative_base()

class User(Base):
    __tablename__ = 'users'

    id = Column(Integer, primary_key=True)
    firstname = Column(String(50))
    lastname = Column(String(50))

    @hybrid_property
    def fullname(self):
        return self.firstname + " " + self.lastname

    @fullname.setter
    def fullname(self, fullname):
        self.firstname, self.lastname = fullname.split(" ")

    @fullname.expression
    def fullname(cls):
        return cls.firstname + " " + cls.lastname

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

Session = sessionmaker(bind=engine)
session = Session()

user1 = User(firstname='Alice', lastname='Smith')
session.add(user1)
session.commit()

print(user1.fullname)  # 输出: Alice Smith

user1.fullname = "Bob Johnson"
session.commit()

print(user1.firstname)  # 输出: Bob
print(user1.lastname)   # 输出: Johnson

# 在查询中使用fullname
users = session.query(User).filter(User.fullname == "Bob Johnson").all()
print(users)
  1. 事件监听 (Events):

SQLAlchemy允许你监听各种数据库事件,例如插入、更新、删除等。这对于实现一些自定义的业务逻辑非常有用。

from sqlalchemy import event
from sqlalchemy import Column, Integer, String
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker

Base = declarative_base()

class User(Base):
    __tablename__ = 'users'

    id = Column(Integer, primary_key=True)
    name = Column(String(50))

@event.listens_for(User, 'before_insert')
def before_insert_listener(mapper, connection, target):
    print(f"Before inserting user: {target.name}")
    target.name = target.name.upper()  # 将用户名转换为大写

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

Session = sessionmaker(bind=engine)
session = Session()

user1 = User(name='Alice')
session.add(user1)
session.commit()

print(user1.name) # 输出: ALICE
  1. 使用反射 (Reflection):

如果你已经有一个现有的数据库,可以使用反射来自动生成Table对象或声明式类。

from sqlalchemy import create_engine, MetaData
from sqlalchemy.orm import sessionmaker

engine = create_engine('sqlite:///existing.db') # 假设有一个现有的数据库

metadata = MetaData()
metadata.reflect(bind=engine)

users_table = metadata.tables['users'] # 获取 users 表的 Table 对象

# 使用经典式映射
from sqlalchemy.orm import mapper
class User:
    pass

mapper(User, users_table)

Session = sessionmaker(bind=engine)
session = Session()

# 现在你可以像使用经典式映射一样使用 User 类

对于声明式映射,可以用 automap 扩展:

from sqlalchemy import create_engine
from sqlalchemy.ext.automap import automap_base
from sqlalchemy.orm import Session

engine = create_engine("sqlite:///existing.db")

Base = automap_base()

# reflect the tables
Base.prepare(engine, reflect=True)

# mapped classes are now created with names by default
# matching that of the table name.
User = Base.classes.users

session = Session(engine)

# 同样可以像声明式映射一样使用 User 类

总结:

SQLAlchemy的声明式和经典式映射是两种强大的工具,可以帮助你轻松地把Python类映射到数据库表。选择哪种映射方式取决于你的具体需求和偏好。希望通过今天的讲解,大家能对这两种映射方式有更深入的了解,并在实际项目中灵活运用。

好了,今天的讲座就到这里,希望大家有所收获!下次再见!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注