sqlalchemy hybrid_attribute表达式

时间:2019-04-10 22:38:16

标签: python sqlalchemy flask-sqlalchemy

假设以下模型:

config.templateBody = `{
  "Parameters" : {
      "ThingName" : {
          "Type" : "String"
      },
      "SerialNumber" : {
          "Type" : "String"
      },
      "Landscape" : {
          "Type" : "String",
          "Default" : "WA"
      },
      "CertificateId" : {
          "Type" : "String"    
      }
  },
  "Resources" : {
      "thing" : {
          "Type" : "AWS::IoT::Thing",
          "Properties" : {
              "ThingName" : {"Ref" : "ThingName"},
              "AttributePayload" : { "version" : "v1", "serialNumber" :  {"Ref" : "SerialNumber"}, "Landscape" : {"Ref" : "Landscape"}}, 
              "ThingTypeName" :  "EC"
          }
      },
      "certificate" : {
          "Type" : "AWS::IoT::Certificate",
          "Properties" : {
              "CertificateId": {"Ref" : "CertificateId"}
          }
      },
      "policy" : {
          "Type" : "AWS::IoT::Policy",
          "Properties" : {
              "PolicyDocument" : "{ \"Version\": \"2012-10-17\", \"Statement\": [{ \"Effect\": \"Allow\", \"Action\":[\"iot:Publish\"], \"Resource\": \"*\" }] }"
          }
      }
  }   
}`

var params = {
                templateBody:config.templateBody,
                parameters:{
                    ThingName: serialNumber,
                    SerialNumber: serialNumber,
                    Landscape: landscape,
                    CertificateId: '<id>'
                },
            }

    const iot = new AWS.Iot();
    iot.registerThing(params, function(err, data){
                if (err) {
                    console.log("[applycert] Error while registering thing:" + err.stack);
                    callback(null, err);
                }

此查询提供了正确的结果:

class Worker(Model):
    __tablename__ = 'workers'
    ...
    jobs = relationship('Job',
                        back_populates='worker',
                        order_by='desc(Job.started)',
                        lazy='dynamic')

    @hybrid_property
    def latest_job(self):
        return self.jobs.first()  # jobs already ordered descending

    @latest_job.expression
    def latest_job(cls):
        Job = db.Model._decl_class_registry.get('Job')
        return select([func.max(Job.started)]).where(cls.id == Job.worker_id).as_scalar()

class Job(Model):
    ...
    started = db.Column(db.DateTime, default=datetime.utcnow)
    worker_id = db.Column(db.Integer, db.ForeignKey('workers.id'))
    worker = db.relationship('Worker', back_populates='jobs')

我假设我可以直接查询该字段,但是此查询失败:

db.session.query(Worker).join(Job.started).filter(Job.started >= datetime.datetime(2017, 5, 10, 0, 2, 45, 932983)).distinct().count()

出现此错误:

db.session.query(Worker).join(Job).filter(Worker.latest_job.started >= datetime.datetime(2017, 5, 10, 0, 2, 45, 932983)).count()

如何直接查询此属性?我在这里想念什么?

编辑1: 遵循@Ilja的建议,我尝试过:

AttributeError: Neither 'hybrid_property' object nor 'ExprComparator' object associated with Worker.latest_job has an attribute 'started'

但出现此错误:

db.session.query(Worker).\
    join(Job).\
    filter(Worker.latest_job >= datetime.datetime(2017, 5, 10, 0, 2, 45, 932983)).\
    count()

1 个答案:

答案 0 :(得分:1)

在SQL(类)上下文中使用时,您将从混合属性返回标量子查询,因此只需使用它就可以使用值表达式:

db.session.query(Worker).\
    filter(Worker.latest_job >= datetime.datetime(2017, 5, 10, 0, 2, 45, 932983)).\
    count()

在这种情况下,hybrid属性本身需要显式处理关联:

@latest_job.expression
def latest_job(cls):
    Job = db.Model._decl_class_registry.get('Job')
    return select([func.max(Job.started)]).\
        where(cls.id == Job.worker_id).\
        correlate(cls).\
        as_scalar()

请注意,混合属性的Python端和SQL端之间存在一些不对称性。与在SQL中生成相关的标量子查询Job相比,它在实例上访问时会生成最新的max(started)对象。如果您还希望它在SQL中返回一个Job行,则可以执行类似的操作

@latest_job.expression
def latest_job(cls):
    Job = db.Model._decl_class_registry.get('Job')
    return select([Job]).\
        where(cls.id == Job.worker_id).\
        order_by(Job.started.desc()).\
        limit(1).\
        correlate(cls).\
        subquery()

但是实际上实际上用处不大,因为通常(但并非总是如此)这种相关子查询比结合子查询慢。例如,为了获取具有原始条件的最新职位的工人:

job_alias = db.aliased(Job)
# This reads as: find worker_id and started of jobs that have no matching
# jobs with the same worker_id and greater started, or in other words the
# worker_id, started of the latest jobs.
latest_jobs = db.session.query(Job.worker_id, Job.started).\
    outerjoin(job_alias, and_(Job.worker_id == job_alias.worker_id,
                              Job.started < job_alias.started)).\
    filter(job_alias.id == None).\
    subquery()

db.session.query(Worker).\
    join(latest_jobs, Worker.id == latest_jobs.c.worker_id).\
    filter(latest_jobs.c.started >= datetime.datetime(2017, 5, 10, 0, 2, 45, 932983)).\
    count()

当然,如果您只想计数,则根本不需要联接:

job_alias = db.aliased(Job)
db.session.query(func.count()).\
    outerjoin(job_alias, and_(Job.worker_id == job_alias.worker_id,
                              Job.started < job_alias.started)).\
    filter(job_alias.id == None,
           Job.started >= datetime.datetime(2017, 5, 10, 0, 2, 45, 932983)).\
    scalar()

请注意,对Query.scalar()的调用与Query.as_scalar()不同,只是返回第一行的第一个值。