sqlalchemy.orm中validates对两个字段进行联合校验

版本

sqlalchemy==1.4.37

需求说明

有个场景,需要在orm中对两个字段进行联合校验,当 col1 ='xxx’时,对 col2的长度进行检查,超过限制(500)时,进行截断。

网上找了很久,没找到类似的实现,自己摸索出来了一套方法;

解决

在 validates 装饰器中,它是在设置字段值之前被调用的,validates 包装的函数校验完成后通过return赋值给字段

validates 的执行顺序 看起来是和 字段 传入ORM模型的顺序 一样。
如 下面案例 中的 model_instance 中,如果 col1 在 col2 之前,就会先校验和赋值 col1 ,反之,则先校验和赋值 col12

方案1

保证 model_instance 中 字段 col1 在 col2 之前,这样会先校验和赋值 col1 ,在校验 col2 时,self.col1 就不会为空,能正常进行校验。

如果先赋值col2,在 validate_col2 中,会self.col1会为None,导致校验失败

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

Base = declarative_base()

class MyModel(Base):
    __tablename__ = 'my_model'
    id = Column(Integer, primary_key=True)
    col1 = Column(String(50))
    col2 = Column(String(500))  # 假设col2的最大长度是500个字符

    @validates('col2')
    def validate_col2(self, key, value):
        # 检查col1的值是否为'xxx'
        if self.col1 == 'xxx':
            # 如果col1是'xxx',则校验col2的长度
            if len(value) > 500:
                value = value[:500]
        # 如果col1不是'xxx',可以选择不做任何操作或者添加其他逻辑
        return value 

# 示例使用1
# 先赋值 col1 再赋值 col2
model_instance = MyModel()
model_instance.col1 = 'xxx'  # 假设这是触发条件的值
model_instance.col2 = 'a' * 501  # 这将触发长度校验

# 示例使用2
datas = {'col1'= 'xxx', 'col2': 'a' * 501 }
# 先pop删除,再添加,就不管前面datas是怎么来的,可以保证 datas 中 col2会比col1后遍历到
_col2_v = datas.pop('col2')
datas['col2'] = _col2_v  
model_instance = MyModel(**datas )


try:
    # 假设这是保存模型到数据库的代码
    # session.add(model_instance)
    # session.commit()
    pass
except ValueError as e:
    print(e)

方案2

实例化orm模型后,再调用一遍 validate_col2 ,校验并赋值给col2

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

Base = declarative_base()

class MyModel(Base):
    __tablename__ = 'my_model'
    id = Column(Integer, primary_key=True)
    col1 = Column(String(50))
    col2 = Column(String(500))  # 假设col2的最大长度是500个字符

    @validates('col2')
    def validate_col2(self, key, value):
        # 检查col1的值是否为'xxx'
        if self.col1 == 'xxx':
            # 如果col1是'xxx',则校验col2的长度
            if len(value) > 500:
                value = value[:500]
        # 如果col1不是'xxx',可以选择不做任何操作或者添加其他逻辑
        return value 

# 示例使用1
# 先赋值 col1 再赋值 col2
model_instance = MyModel()
model_instance.col1 = 'xxx'  # 假设这是触发条件的值
model_instance.col2 = 'a' * 501  # 这将触发长度校验

# 示例使用2
datas = {'col1'= 'xxx', 'col2': 'a' * 501 }

model_instance = MyModel(**datas )
# 上面实例化会自动调用所有的 `validates` 函数,下面再调用一遍`validate_col2`, 
# 并且要用 `model_instance.col2` 接收返回值,不然 `model_instance.col2` 的值不会改变。
model_instance.col2 = model_instance.validate_col2('col2', datas['col2'])


try:
    # 假设这是保存模型到数据库的代码
    # session.add(model_instance)
    # session.commit()
    pass
except ValueError as e:
    print(e)

方案3

无法保证 model_instance 中 字段 col1 在 col2 之前的顺序,采用 临时变量 __col1,存储 col1 的值,并对 col2 进行二次校验赋值

validate_col1 函数中,校验 col1,先把 value 值(就是没校验前的col1的值)赋给 self.__col1,然后再调用 validate_col1_and_col2 进行联合校验,最后通过 return把value赋值给 self.col1

在整个过程中,validate_col1_and_col2 会被调用3次

  • 校验 col2 时,validate_col2 会调用一次
  • 校验 col1 时,self.col2 = self.validate_col1_and_col2(key='col2', value=self.col2) 这一行会调用两次:
    1. 一次是 self.validate_col1_and_col2 执行;
    2. 另一次是 1 执行完后对 self.col2 赋值,会调用一次 validate_col2 ,进而再调用一次
from sqlalchemy.orm import validates
from sqlalchemy import Column, String, Integer
from sqlalchemy.ext.declarative import declarative_base



def getStrLenAndTruncate(ss: str, max_length=500):
    """
    获取字符串长度,超过部分截断
    :param ss:
    :param max_length:
    :return:
    """
    slen = len(ss.encode('utf-8'))

    # 如果编码后的字符串长度小于或等于500字节,则不需要截断
    if slen <= max_length:
        return ss

    # 截断到500字节的长度,注意这里直接截断可能会导致字符不完整
    # 因此需要找到一个合适的截断点,确保截断后的字符串是完整的utf-8字符
    truncated_encoded = b''
    current_length = 0
    for char in ss:
        char_encoded = char.encode('utf-8')
        if current_length + len(char_encoded) <= max_length:
            truncated_encoded += char_encoded
            current_length += len(char_encoded)
        else:
            break

    truncated_str = truncated_encoded.decode('utf-8', errors='ignore')
    print(f'原字符串编码后长度为{slen}, 超过限制{max_length}, 需进行截断\n原字符串={ss}, \n截断后字符串:{truncated_str}')
    return truncated_str


Base = declarative_base()


class MyModel(Base):
    __tablename__ = 'my_model'
    __col1 = ''
    id = Column(Integer, primary_key=True)
    col1 = Column(String(50))
    col2 = Column(String(500))  # 假设col2的最大长度是500个字符
	

    @validates('col1')
    def validate_col1(self, key, value):
        self.__col1 = value
        self.col2 = self.validate_col1_and_col2(key='col2', value=self.col2)
        return value
        

    @validates('col2')
    def validate_col2(self, key, value):
        value = validate_col1_and_col2(key, value)
        return value 
    
    def validate_col1_and_col2(self, key, value):
        # 检查col1的值是否为'xxx'
    	if self.__col1== 'xxx':
    	    if not value:
                value = ''
            elif len(value) * 3 <= 500:
                # 存储到 oracle,中文占3个字符
                pass
            else:
                print('需检查 col2 长度')
                value = getStrLenAndTruncate(value)

        return value
 
# 示例使用
datas = {'col1'= 'xxx', 'col2': 'a' * 501 }
model_instance = MyModel(**datas )


try:
    # 假设这是保存模型到数据库的代码
    # session.add(model_instance)
    # session.commit()
    pass
except ValueError as e:
    print(e)

说明

为啥不省略下面这个 validate_col2 这个校验代码:

    @validates('col2')
    def validate_col2(self, key, value):
        value = validate_col1_and_col2(key, value)
        return value 

因为 这个方案中,col1 、col2进入 orm模型的顺序不一定,如果省略了validate_col2 ,当col1比col2先进入模型,那么在 validate_col1 调用 self.validate_col1_and_col2(key='col2', value=self.col2) 时,self.col2其实等于None,此时对col2校验是没有意义的。等到 col2 进入 orm模型,又缺少对它进行校验的函数。

注意:

不能在 某个字段的校验函数中对其进行赋值操作,不然会陷入递归循环,因为赋值操作会调用校验函数;

如下面的调用会陷入递归死循环,因为 self.col1 = value 这行代码对 self.col1 进行了赋值,会自动再次调用validate_col1校验函数,就会在这一行陷入递归死循环而报错。


class MyModel(Base):
    @validates('col1')
    def validate_col1(self, key, value):
        self.__col1 = value
        self.col1 = value
        self.col2 = self.validate_col1_and_col2(key='col2', value=self.col2)
        return value
        

其他方案

不走orm模型,直接写校验代码和原生sql处理。

最后

这就是我尝试出来的 在 sqlalchemy.orm中validates对两个字段进行联合校验的方法,总感觉不太完美,不知道有没有大佬知道更好的方案,欢迎分享

<think>好的,用户现在问的是如何在现有的数据库模型基础上新增一条任务数据。用户已经提供了两个模型类,Template和Task,其中Task有一个外键关联到Template的id。我需要先仔细分析用户提供的代码结构,然后给出正确的插入方法。 首先,确认用户的问题。用户已经定义了Task模型,现在想知道如何向task表中插入一条新记录。需要确保所有必填字段都被正确赋值,特别是外键template_id必须对应存在的Template记录。 接下来,回顾用户提供的模型定义。Task表的字段包括id、name、state、creator、start_time、end_time、data、node、template_id,其中template_id是外键,指向template表的id。所有字段都是nullable=False,所以新增记录时必须为这些字段提供值,否则会引发数据库错误。 用户可能遇到的问题包括:如何创建Task实例,如何设置外键,如何处理时间字段(start_time和end_time是Integer类型,可能存储时间戳),以及如何提交到数据库。 需要分步骤说明如何创建新任务。首先,确保对应的Template存在,可以通过template_id查询。然后,创建Task实例,填充所有必填字段。这里需要注意数据类型,比如data和node字段是Text类型,可能需要JSON字符串。时间字段是整数,可能需要使用时间戳。然后,将实例添加到会话,并提交事务。 还要提醒用户注意外键约束,确保template_id对应的Template存在,否则插入会失败。另外,可能还需要处理关系,比如通过Template的tasks反向引用来添加任务,但用户的问题可能更倾向于直接创建Task实例。 最后,可能需要提供示例代码,展示具体的实现步骤,包括导入模型、创建实例、添加到会话、提交等。同时,提醒异常处理,比如捕获数据库错误,回滚会话等。 可能还需要解释to_dict方法的作用,但用户的问题主要是新增数据,所以可能不需要深入讨论这个方法。不过,如果用户想验证数据是否正确插入,可以使用这个方法将对象转换为字典查看。 总结回答的步骤:1. 准备必要数据,特别是外键;2. 创建Task实例;3. 添加到数据库会话;4. 提交事务。同时给出代码示例和注意事项,如字段类型、外键存在性、时间戳处理等。</think>以下是基于SQLAlchemy模型新增任务数据的完整实现步骤和示例代码: --- ### 步骤解析 1. **外键验证**:确保`template_id`对应的模板存在 2. **时间处理**:`start_time`和`end_time`需要整数时间戳 3. **数据序列化**:`data`和`node`字段需要JSON字符串格式 4. **事务提交**:使用数据库会话提交 --- ### 完整实现代码 ```python from datetime import datetime import json from your_application import db # 替换为实际导入方式 def create_new_task( task_name: str, creator: str, template_id: int, global_vars: dict, node_info: dict, process_data: dict ) -> Task: """ 创建新任务 :param task_name: 任务名称(对应name字段) :param creator: 创建人(对应creator字段) :param template_id: 模板ID(对应template_id字段) :param global_vars: 全局变量字典(自动序列化为JSON字符串) :param node_info: 结点信息字典(自动序列化为JSON字符串) :param process_data: 前端结点信息字典(自动序列化为JSON字符串) :return: 创建成功的Task对象 """ # 验证模板是否存在 if not Template.query.get(template_id): raise ValueError(f"模板ID {template_id} 不存在") # 生成时间戳(秒级) timestamp = int(datetime.now().timestamp()) # 创建任务对象 new_task = Task( name=task_name, state='pending', # 初始状态设为等待中 creator=creator, start_time=timestamp, end_time=timestamp, # 初始结束时间同开始时间,任务完成后更新 data=json.dumps(global_vars, ensure_ascii=False), node=json.dumps(node_info, ensure_ascii=False), process_data=json.dumps(process_data, ensure_ascii=False), template_id=template_id ) try: db.session.add(new_task) db.session.commit() return new_task except Exception as e: db.session.rollback() raise RuntimeError(f"创建任务失败: {str(e)}") ``` --- ### 使用示例 ```python # 准备测试数据 task_data = { "task_name": "月度报表生成任务", "creator": "张伟", "template_id": 1, # 假设存在的模板ID "global_vars": {"report_month": "2023-08", "department": "财务部"}, "node_info": {"nodes": [{"id": 1, "type": "start"}]}, "process_data": {"connections": [{"from": 1, "to": 2}]} } try: new_task = create_new_task(**task_data) print(f"创建成功,任务ID:{new_task.id}") print("任务详情:", new_task.to_dict()) except ValueError as ve: print(f"参数错误:{str(ve)}") except RuntimeError as re: print(f"数据库错误:{str(re)}") ``` --- ### 关键字段说明 | 字段名 | 类型 | 要求 | 示例 | |--------|------|------|------| | name | String(256) | 唯一任务名称 | "客户数据同步任务" | | state | String(256) | 状态标识 | "running"/"success"/"failed" | | creator | String(256) | 创建人标识 | "user123" | | start_time | Integer | 秒级时间戳 | 1690761600 | | data | Text | JSON字符串 | `{"param1": "value1"}` | | node | Text | JSON字符串 | `{"nodes": [...]}` | | template_id | Integer | 必须存在的模板ID | 1 | --- ### 最佳实践建议 1. **状态枚举管理**: ```python class TaskStatus: PENDING = 'pending' RUNNING = 'running' SUCCESS = 'success' FAILED = 'failed' ``` 调用时使用`state=TaskStatus.PENDING` 2. **时间自动生成**: ```python # 在Task模型中添加自动时间管理 created_at = Column(Integer, default=lambda: int(datetime.now().timestamp())) updated_at = Column(Integer, onupdate=lambda: int(datetime.now().timestamp())) ``` 3. **数据校验装饰器**: ```python from sqlalchemy.orm import validates class Task(db.Model): @validates('data', 'node', 'process_data') def validate_json_fields(self, key, value): try: json.loads(value) return value except json.JSONDecodeError: raise ValueError(f"{key}必须为有效JSON字符串") ``` --- ### 关联操作说明 当成功创建任务后,可以通过模板关系反向查询: ```python template = Template.query.get(1) latest_task = template.tasks.order_by(Task.id.desc()).first() ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值