Python:只需编写 SQL

FROM Python: Just write SQL (joaodlf.com)

对于那些不熟悉的人来说,Go 更喜欢使用非 ORM、非查询构建器方法来与数据库交互。这是自然而然地归因于 sql 包:与数据库驱动程序一起使用的通用接口。即使在大型项目中,在 Go 中看到实际的 SQL 也是很常见的。

另一方面,Python的标准库中没有任何支持数据库交互的东西,这一直是社区需要解决的问题。 Python 有许多 ORM 和查询生成器:

当然,您可以使用您最喜欢的适配器并用 Python 编写 SQL(这就是我们要做的!),但我认为大多数开发人员都会同意,一旦您需要与数据库进行交互,它就远远不止如此。可能会立即使用像 SQLAlchemy 这样的 ORM。

(这并不是说 Go 没有流行的 ORM,但你更有可能看到 Go 开发人员利用标准库和数据库适配器,这是新 Go 开发人员将采用的典型方法。)

在这篇文章中,我的目标是像 Go 一样在 Python 中处理 SQL:

  • 我想写SQL。
  • 我不想依赖查询生成器(更不用说 ORM)。
  • 我想将所有这些打包在一个抽象中,使我能够在数据库解决方案之间快速更改,并使其易于测试。
  • 我希望我的数据库和业务逻辑之间有非常清晰的分离。

我们将从一个典型的例子开始:

python 复制代码
# user/domain.py

from dataclasses import dataclass
import datetime
from typing import Optional


@dataclass
class User:
    id: int = None
    dt_created: datetime = None
    username: str = None
    email: str = None
    mobile: Optional[str] = None

User 是您可能看到的名为 user 的 SQL 表的字面定义。这包括我喜欢包含在所有 SQL 表中的一些列:

  • id (主键)。
  • dt_created 日期时间值,通常默认为 NOW()

所有字段默认为 None ,这将允许我们执行不包含表中所有列的 SELECT 查询,同时仍然能够将行转换为 User 对象。除了 mobile 之外,所有列都是 NOT NULL ,这里我使用 Optional 类型提示来强调该字段允许 NULL .

我们现在可以创建我们的存储库:

python 复制代码
# user/repository.py

from abc import ABC, abstractmethod

from user.domain import User


class UserRepository(ABC):
    @abstractmethod
    def new(self, new_row: User) -> int:
        """
        Create a new record and returns the new id value.
        """
        pass

    @abstractmethod
    def get_by_id(self, id_val: int) -> User:
        """
        Get a single record by ID value.
        """
        pass 

我喜欢我的存储库是抽象类,这可以确保任何未来的实现都需要遵循合同(尽可能使用 Python,最好的意图等等)。

我们的第一个实现是通过 [psycopg3]www.psycopg.org/psycopg3/do...) 实现PostgreSQL:

python 复制代码
import psycopg
from psycopg.rows import class_row

# Load up your config from somewhere...
from config import get_config

cfg = get_config()

conn = psycopg.connect(
    dbname=cfg.database_name,
    user=cfg.database_username,
    password=cfg.database_password,
    host=cfg.database_host,
)


def new_cursor(name=None, row_factory=None):
    if name is None:
        name = ""

    if row_factory is None:
        return conn.cursor(name=name)

    return conn.cursor(name=name, row_factory=class_row(row_factory))

首先,我们设置连接。其次,返回 psycopg 游标的函数。 custom row factories 包括自定义行工厂,我们很快就会看到这个功能有多么有用。

我们现在实现存储库:

python 复制代码
# user/repository.py

from external.postgres import conn, new_cursor


from user.domain import User

class UserPostgreSQL(UserRepository):
    def new(self, new_row: User) -> int:
        with new_cursor() as cur:
            cur.execute(
                """
            INSERT INTO users
            (username, email)
            VALUES
            (%s, %s)

            RETURNING id;
            """,
                (
                    new_row.username,
                    new_row.email
                ),
            )
            
            new_id = cur.fetchone()
            if new_id and len(new_id):
                conn.commit()
                new_id = new_id[0]
            else:
                new_id = 0
        
        return new_id

    def get_by_id(self, id_val: int) -> User:
        with new_cursor(name="get_by_id", row_factory=User) as cur:
            cur.execute(
                """
            SELECT *
            FROM users
            WHERE id = %s
            """,
                (id_val,),
            )
            
            return cur.fetchone()
            
def new_user_repo() -> UserRepository:
    return UserPostgreSQL()

您会注意到 new() 使用基本游标,但 get_by_id() 使用带有类行工厂的游标,这意味着返回的行将是 User 类型。漂亮。

new_user_repo() 是我们在假设的业务逻辑中使用的函数,实现本身永远不会公开:

python 复制代码
from user.repository import *  
  
user_repo = new_user_repo()  
  
row = User()  
row.username = "joao"  
row.email = "joao@nospam.com"  
  
# Create a new record.  
new_id = user_repo.new(row)  
  
# Fetch the record we just created.  
new_row = user_repo.get_by_id(new_id)
# User(id=1, dt_created=datetime.datetime(2023, 8, 9, 23, 8, 14, 974074, tzinfo=datetime.timezone.utc), username='joao', email='joao@nospam.com', mobile=None)

new_user_repo() 可以修改为返回任何实现 UserRepository 的内容,可能是 MySQL 实现,也可能是 SQLite 实现,甚至可能是用于测试目的的模拟。

plus

python 复制代码
# config.py
class get_config:
    database_name = "test_db"
    database_username = "yulik"
    database_password = "password"
    database_host = "localhost"
sql 复制代码
CREATE TABLE users (
    id SERIAL PRIMARY KEY,
    dt_created TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    username VARCHAR(100) NOT NULL,
    email VARCHAR(255),
    mobile VARCHAR(20)
);

结论

我在技术上花了足够多的时间,看到语言和框架失宠,库和工具来来去去。 SQL 一直是一个常数,我看到了编写它的巨大好处。根据我的经验,任何能让您更接近数据库安装的事情(即使在云时代)都是一件好事。

上面的实现并没有突破任何新领域,事实上,它甚至可能看起来与 ORM API 非常相似......但这始终是我们的目标:编写原始 SQL 并不一定意味着非结构化代码。

我希望至少有一位 Python 开发人员在下次启动新项目时重新考虑他们的 SQL 方法。

完整测试的代码库 github.com/yulk/Python...

相关推荐
Y200309162 小时前
使用 PyTorch 实现 MNIST 手写数字识别
python
马尚来2 小时前
移动端自动化测试Appium,从入门到项目实战Python版
python
天才测试猿2 小时前
WebUI自动化测试:POM设计模式全解析
自动化测试·软件测试·python·selenium·测试工具·设计模式·测试用例
MonkeyKing_sunyuhua2 小时前
python线程间怎么通信
android·网络·python
跳跳的向阳花2 小时前
01、大模型部署方案与Dify的使用
python
西柚小萌新3 小时前
【Python从入门到精通】--Pycharm增加内存
开发语言·python·pycharm
西柚小萌新3 小时前
【深入浅出PyTorch】--7.1.PyTorch可视化1
人工智能·pytorch·python
我是华为OD~HR~栗栗呀3 小时前
华为OD-23届考研-Java面经
java·c++·后端·python·华为od·华为·面试
Small___ming3 小时前
【Python基础】Python路径操作全解析:os.path、glob与pathlib从入门到精通
开发语言·python
这里有鱼汤3 小时前
3步用Python识别MACD背驰,避免80%追涨杀跌陷阱,建议收藏
后端·python