Geometry('POINT') 列作为 str 对象返回

Geometry('POINT') column being returned as str object

我有一个包含以下列的 sqlalchemy 模型对象:

gps = Column(Geometry('POINT'))

我在模型 class 中实现了一个 to_dict 函数,为此我需要解构 gps 对象以提供纬度和经度。这在另一个模型中成功地为我工作。但是由于某种原因,在问题的 class 中,以下代码导致属性错误 ('str' object has no attribute 'data' ):

point = wkb.loads(bytes(self.gps.data))

我这样存储 gps 数据:

gps = Point(longitude, latitude).wkt

这是来自 postgresql 的 table 描述:

   Column    |            Type             |                     Modifiers                     | Storage | Stats target | Description 
-------------+-----------------------------+---------------------------------------------------+---------+--------------+-------------
 id          | integer                     | not null default nextval('pins_id_seq'::regclass) | plain   |              | 
 gps         | geometry(Point)             |                                                   | main    |              | 

我在 Pin 对象创建后立即调用 as dict 方法,如下所示:

gps = Point(
        float(data['longitude']),
        float(data['latitude'])
    ).wkt
pin = Pin(gps=gps)
# Commit pin to disk 
# otherwise fields will 
# not return properly
with transaction.manager:
    self.dbsession.add(pin)
    transaction.commit()
    print (pin.as_dict())

让我发疯的是,某些代码确实适用于其他模型。任何见解将不胜感激。

编辑:根据 Ilja 的评论,我了解到问题是对象没有写入磁盘,显然几何列将被视为字符串直到那发生。但是即使现在我也遇到同样的错误。基本上,在这一点上,transaction.commit() 函数没有做我认为应该做的事情......

与此相关的是会话对象的配置。由于所有这些都在 Pyramid web 框架下,我使用默认会话配置,如 here 所述(您可以跳过前几段,直到他们开始讨论 /models/__init__.py 文件。Ctrl + F 如果需要)。

如果我遗漏了一些重要的细节,请在下面重现有问题的 class:

from geoalchemy2 import Geometry
from sqlalchemy import (
    Column,
    Integer,
)
from shapely import wkb

from .meta import Base


class Pin(Base):
    __tablename__ = 'pins'
    id = Column(Integer, primary_key=True)
    gps = Column(Geometry('POINT'))

    def as_dict(self):
        toret = {}
        point = wkb.loads(bytes(self.gps.data))
        lat = point.x
        lon = point.y
        toret['gps'] = {'lon': lon, 'lat': lat}
        return toret

一开始我以为是

的原因
Traceback (most recent call last):
  ...
  File "/.../pyramid_test/views/default.py", line 28, in my_view
    print(pin.as_dict())
  File "/.../pyramid_test/models/pin.py", line 18, in as_dict
    point = wkb.loads(bytes(self.gps.data))
AttributeError: 'str' object has no attribute 'data'

zope.sqlalchemy 在提交时关闭会话,但留下未过期的实例,但事实并非如此。这是由于前段时间使用了 Pyramid,当时全局 transaction 仍然会影响请求期间正在进行的事务,但现在默认似乎是 explicit transaction manager.

实际问题是transaction.commit()对当前会话正在进行的事务没有影响。添加一些日志记录将使这一点变得清晰:

with transaction.manager:
    self.dbsession.add(pin)
    transaction.commit()
    print("Called transaction.commit()")
    insp = inspect(pin)
    print(insp.transient,
          insp.pending,
          insp.persistent,
          insp.detached,
          insp.deleted,
          insp.session)

这导致大约:

 % env/bin/pserve development.ini   
2018-01-19 14:36:25,113 INFO  [shapely.speedups._speedups:219][MainThread] Numpy was not imported, continuing without requires()
Starting server in PID 1081.
Serving on http://localhost:6543
...
Called transaction.commit()
False True False False False <sqlalchemy.orm.session.Session object at 0x7f958169d0f0>
...
2018-01-19 14:36:28,855 INFO  [sqlalchemy.engine.base.Engine:682][waitress] BEGIN (implicit)
2018-01-19 14:36:28,856 INFO  [sqlalchemy.engine.base.Engine:1151][waitress] INSERT INTO pins (gps) VALUES (ST_GeomFromEWKT(%(gps)s)) RETURNING pins.id
2018-01-19 14:36:28,856 INFO  [sqlalchemy.engine.base.Engine:1154][waitress] {'gps': 'POINT (1 1)'}
2018-01-19 14:36:28,881 INFO  [sqlalchemy.engine.base.Engine:722][waitress] COMMIT

可以看出没有提交发生并且实例仍在 pending state, and so its gps attribute holds the text value from the assignment. If you wish to handle your serialization the way you do, you could first flush the changes to the DB and then expire 实例属性中:

gps = Point(
        float(data['longitude']),
        float(data['latitude'])
    ).wkt
pin = Pin(gps=gps)
self.dbsession.add(pin)
self.dbsession.flush()
self.dbsession.expire(pin, ['gps'])  # expire the gps attr
print(pin.as_dict())  # SQLAlchemy will fetch the value from the DB

另一方面,您也可以避免在应用程序中处理 (E)WKB 表示并直接使用 column_property() 访问器从数据库请求坐标:

class Pin(Base):
    __tablename__ = 'pins'
    id = Column(Integer, primary_key=True)
    gps = Column(Geometry('POINT'))
    gps_x = column_property(gps.ST_X())
    gps_y = column_property(gps.ST_Y())

    def as_dict(self):
        toret = {}
        toret['gps'] = {'lon': self.gps_y, 'lat': self.gps_x}
        return toret

这样一来,手动 expire(pin) 就变得不必要了,因为在这种情况下列属性无论如何都必须自行刷新。当然,由于您在构建新的 Pin 时已经知道坐标,因此您可以预先填充它们:

lon = float(data['longitude'])
lat = float(data['latitude'])
gps = Point(lon, lat).wkt
pin = Pin(gps=gps, gps_x=lat, gps_y=lon)

因此甚至不需要刷新、过期和提取。