如何使用GeoAlchemy和Spatialite支持您数据库中的空间数据

422 阅读12分钟

使用GeoAlchemy和Spatialite支持你数据库中的空间数据

在构建某些类型的应用程序时,重要的是要知道你将处理什么样的数据,这样你就可以为持有和处理它们做充分的准备。

其中一个例子就是当你需要处理 "空间数据"(位置数据--以经度和纬度为单位)时。此外,你可能希望能够根据这些信息进行查询。

例如,你可以设想在数据库中查询 "离给定位置(经度、纬度)最近的10个实体"。

在这篇文章中,我们将使用SQLite及其空间扩展,为一个简单的flask服务器创建一个支持空间数据读写操作的数据库。

我们还将看到如何从我们的服务器上的数据库读写空间数据。

构建一个简单的网络服务器

我们将从为一个服务打车平台创建一个简单的后台服务器开始,在这个平台上,用户可以与他们附近的 "服务提供商 "联系,以利用收费的服务。

对于这种平台,我们必须处理空间信息,如 "客户的位置"、"服务提供者的位置",而且我们可能还想对数据库进行查询,以便找到离某个位置最近的10个服务提供者。

本节展示了如何为后端服务器进行设置。

先决条件

  • 已安装Python3
  • 基本的Flask知识

我们将首先在根目录下创建一个名为server 的文件夹。我们将添加一个名为core 的新文件夹,以及另外三个名为wsgi.pymodels.pyconfig.py 的文件。

core 文件夹内,添加两个新的文件,名为。__init__.py, 和views.py

一旦你完成了,你的server 文件夹应该有一个像下面这样的结构。

server
|   models.py
|   wsgi.py
|   config.py
|___core/
    |   __init__.py
    |   views.py

安装依赖项

从命令提示符/终端,导航到我们刚刚创建的server 文件夹。

现在,使用virtualenv ,为项目创建一个新的虚拟环境。

如果你没有安装virtualenv ,你可以通过pip 进行安装,方法如下。

pip install virtualenv

如果你已经安装了它,你可以跳过这一步,按如下步骤安装我们需要的库。

pip install flask flask-sqlalchemy flask-migrate geoalchemy2

创建和配置Flask服务器

core/ 文件夹的__init__.py 文件中,我们将添加以下几行代码来创建一个应用程序工厂。

这是一个我们调用的函数,用于创建和配置flask应用程序。

这很好,因为它可以接受我们需要的参数,以便在我们调用该函数的任何时候创建一个应用程序。

from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
from config import config_

# initialize extensions
db = SQLAlchemy()
migrate = Migrate()

# app factory
def create_app(config_name):
    # create app instance
    app = Flask(__name__)

    # configure app
    app.config.from_object(config_[config_name])

    # initialize extensions
    db.init_app(app)
    migrate.init_app(app, db)

    return app

让我们在之前创建的config.py 文件中添加一些配置选项。

因此,导航到该文件并添加以下几行代码来添加配置选项。

import os

base_dir = os.path.abspath(os.getcwd())

class Config:
    SQLALCHEMY_TRACK_MODIFICATIONS = False
    SECRET_KEY = os.getenv('app-secret')
    # other generic configuration
    # ..options

class DevConfig(Config):
    SQLALCHEMY_DATABASE_URI = f"sqlite:///{base_dir}/devdb.sqlite"
    DEBUG = True
    FLASK_ENV = 'development'

class ProdConfig(Config):
    SQLALCHEMY_DATABASE_URI = os.getenv('DATABASE_URI')
    FLASK_ENV = 'production'

config_ = {
    'default': DevConfig,
    'development': DevConfig,
    'production': ProdConfig
}

最后一行中的config_ 变量持有一个字典,将每个配置选项映射到相应的环境类型。

这就是我们在__init__.py 文件中,在应用工厂中的做法,如下图所示。

# .. code omitted for brevity
from config import config_
# .. 
# ...
app.config.from_object(config_[config_name])

运行Flask服务器

为了运行我们的flask应用,我们需要在wsgi.py 中添加一些最后的指令,这将有助于我们运行flask应用。

所以,进入该文件并添加以下几行代码。

from core import create_app
import os

# call create_app() to create and configure flask app
app = create_app('default' or os.getenv('FLASK_CONFIG'))

让我们也从终端将FLASK_ENV 环境变量设置为development ,这样我们就不必在每次做出改变时都重新启动我们的服务器。

FLASK_ENV=development

现在,让我们从命令提示符/终端使用以下命令运行我们的flask应用程序。

flask run

注意:这个命令之所以有效,是因为flask默认会寻找任何名为wsgi.pyapp.py 的文件。如果你改变了命名规则,你必须将FLASK_APP环境变量设置为你所使用的文件名。

set FLASK_APP=myfilename.py

一旦你运行这个命令,你应该看到终端上的日志显示服务器正在运行,如下图所示。

server-logs

添加对空间数据的支持

前提是

读者应该已经安装了以下库。

  • [Spatialite]
  • [Pandas]

我们要做的第一件事是为我们将要收集和存储的数据添加模型。这可以通过models.py 文件来完成。

打开该文件并添加以下几行代码。

from core import db
from datetime import datetime as d

# Base class
class Base:
    id = db.Column(db.Integer, primary_key=True, nullable=False)

class User(Base, db.Model):
    name = db.Column(db.String(50))
    email = db.Column(db.String(100), unique=True)
    signup_date = db.Column(db.DateTime, default=d.utcnow())
    profile_photo = db.Column(db.String(200))
    telephone = db.Column(db.String(20), unique=True)
    address = db.Column(db.String(100))
    lon = db.Column(db.Float)
    lat = db.Column(db.Float)
    is_handyman = db.Column(db.Boolean, default=False)
    email_verified = db.Column(db.Boolean, default=False)
    personal_id = db.Column(db.String(200))
    # relationships
    gigs_ = db.relationship('Gig', backref='owner')

class Gig(Base, db.Model):
    title = db.Column(db.String(50))
    description = db.Column(db.Text)
    price = db.Column(db.Float)
    glat = db.Column(db.Float)
    glon = db.Column(db.Float)
    owner_id = db.Column(db.Integer, db.ForeignKey('user.id'))
    date_created = db.Column(db.DateTime(), default=d.utcnow())

我们的模型定义了User 对象和Gig 对象--用户可以是在平台上请求服务的普通客户,也可以是认定为勤杂工(或服务提供者)的用户。

在每一个模型中,我们都会收集lonlat 的值来定义他们的位置。

然而,这些值在几何上没有任何意义(它们只是浮点值),直到我们添加支持,使我们的数据库能够正确处理它们。

安装Spatialite

为了在处理空间数据时添加所需的支持,我们需要安装一个名为Spatialite 的扩展,该扩展为数据库添加支持。

要安装Spatialite ,下载Windows下的二进制文件。

这将把你带到一个像下面所示的页面。

spatialite-download-page

一旦你进入下载页面,你将需要选择第一个选项来下载包含Spatialite二进制文件的压缩文件。

一旦下载完成,你可以使用任何文件提取工具如WinRAR来提取该文件夹。

解压后的文件夹应包含以下文件。

spatialite-folder

接下来,复制这个文件夹的路径,并从终端将其添加为环境变量,称为spat_path

set spat_path=/path/to/your/spatialite/folder

接下来,进入我们创建应用实例的wsgi.py 文件。

我们将添加一些配置选项,以便我们在创建应用程序时可以加载Spatialite 扩展。

更新wsgi.py 文件,使其包含这几行代码。

from sqlalchemy import event
from models import *
import os
import sqlite3

# spatialite path
spatialite_path = os.getenv('spat_path')
os.environ['PATH'] = spatialite_path + ';' + os.environ['PATH']

# create server instance from app factory
app = create_app(os.getenv("FLASK_CONFIG") or "default")

# add extension to sqlite3
with app.app_context():
    @event.listens_for(db.engine, "connect")
    def load_spatialite(dbapi_conn, connection_record):
        dbapi_conn.enable_load_extension(True)
        dbapi_conn.load_extension('mod_spatialite')

在这里,我们可以获取我们存储spatialite扩展位置的spat_path 变量,并将其追加到你电脑上的用户PATH变量。

接下来,我们调用flask应用上下文,允许我们从其原始范围之外访问正在运行的服务器的属性(通常这意味着运行依赖于服务器的运行实例的代码,而不使用原始flask上下文)。

在这个 "上下文 "下,一旦服务器开始运行并加载spatialite扩展,我们就监听connect 事件。

Spatialite 是一个SQLite扩展,只对SQLite起作用。如果你使用的是不同的数据库,比如PostgreSQL,你也需要为它安装扩展(在这种情况下称为PostGIS)。然而,在其他数据库中,你可能也不需要以这种方式加载扩展。这只是为了演示如何在开发环境中完成这一工作。

然后我们需要安装一个新版本的sqlite3 ,因为预装在 Python 中的版本不包括RTree ,这是一个用于处理空间信息的数据结构。

SQLite3的网站上下载最新的版本(可在Windows的预编译二进制文件下获得)。

下载后,解压内容,你应该找到一个sqlite3.dll 文件。

复制该文件并寻找预装在 Python 中的sqlite.dll ,它应该在一个叫做DLLs 的文件夹中。

这将是文件夹的路径。

C:\Users\Paulo\AppData\Local\Programs\Python\Python38\DLLs

然后,把旧的文件复制到一个安全的地方作为备份,并粘贴新下载的文件。

它可能会要求管理员权限,所以授予权限将文件复制到那里,然后我们就可以上路了。

更新模型和创建数据库

现在我们已经在数据库引擎中添加了对空间数据的支持,我们现在将在模型中添加一些特殊的字段,这些字段包含了可以被数据库处理的几何信息。

我们借助于早先安装的GeoAlchemy2 ,去models.py 文件,并更新它,使两个模型包含这些新的字段。

from geoalchemy2 import Geometry

class User(db.Model):
    # .. code ommited for brevity
    # ..
    geometry = db.Column(Geometry(geometry_type='POINT', management = True, srid='4269'))

class Gig(db.Model):
    # .. code ommited for brevity
    # ..
    gig_geometry = db.Column(Geometry(geometry_type='POINT', management = True, srid='4269'))

Geoalchemy2帮助我们定义这些 "几何 "字段,它们可以有WKT(Well Known Text)格式规定的不同geometry_type

这个应用的经度和纬度值将以点的形式出现,如下图所示。

X(lon, lat)

因此,我们指定该字段的geometry_type 属性为POINT ,这可能会根据使用情况而有所不同。

你可以随时查看geolalchemy2 参考文档以了解可用的选项。

创建数据库

我们现在可以开始创建数据库了--我们将首先使用flask_migrate 在模型上运行迁移。

我们已经在前面的__init__.py 文件中为这个扩展添加了配置,所以我们可以初始化迁移,运行它们,并且用我们模型的当前版本升级我们的数据库。

所以,从终端运行下面的命令。

flask db init

这将为我们的数据库初始化迁移,并在我们的项目文件夹中为我们创建一个migrations/ 文件夹,该文件夹中包含了我们模型的元数据,子文件夹名为versions/

我们可以在将这些文件应用到数据库之前对其进行编辑。

接下来,我们运行migrations来创建一个版本文件,供我们进行内省。

flask db migrate

一旦我们运行了 migrations,它就会代表我们创建一个新的版本文件,描述当前的模型,以及一旦我们运行升级命令就会应用到数据库中的变化。

打开项目目录下的migrations/versions/ ,并打开其中的第一个文件,它应该是下面这个样子的。

migrations-script-image

这个文件描述了flask_migrate从模型中看到的变化被应用到数据库中的情况。

这些变化是通过调用这个文件中的upgrade 函数来应用的,并通过调用downgrade 函数来恢复。我们想要的是应用这些变化,所以我们需要调用upgrade

然而,Alembic(也就是flask_migrate的运行环境)并没有在迁移脚本中为我们导入geoalchemy2字段所需的依赖。

我们需要在运行升级函数之前添加这个。否则,我们会得到一个错误,所以我们需要在使用这个脚本之前编辑它。

这就是flask_migrate的灵活之处,因为它允许我们在对数据库运行升级之前进行反省。

在这个版本文件中添加以下导入。

import geoalchemy2

根据Spatialite的文档,在数据库创建之后,我们还必须在任何其他事情之前运行InitSpatialMetaData() 函数。

如果你没有注意到,在我们运行migrate 命令的那一刻,数据库就被创建了。

现在,我们必须在upgrade 函数中加入对这个函数的调用,作为在其他事情之前运行的第一件事。

更新脚本,使upgrade 函数中的第一行内容如下。

def upgrade():-
    conn = op.get_bind()
    conn.execute(sa.text('SELECT InitSpatialMetaData();'))
    # .. 

保存这个文件,并从终端运行升级命令,如下所示。

flask db upgrade

这样就可以用我们的表和其他几个表来更新数据库,这些表是geoalchemy所需要的,以便数据库能够正确地管理我们的数据。

运行查询

让我们用一些用户信息来填充数据库。

我使用Epitools生成了一个经度和纬度值的随机列表,生成的值列表如下所示。

example-spatial-data

这个网站还允许你将其下载为Excel电子表格。

因此,前往该网站获取你的随机经度和纬度值列表。

我不得不对该文件进行一些编辑,删除一些不需要的列和字段,使其看起来像上图所示。这个文件被保存为CSV文件,而不是保存在我们项目目录的根层,以方便访问。

所以我打算用pandas来解析这个文件。

我们可以从一个新的文件中使用做这个。

db.session.commit()

现在,我们有了可以查询和处理的数据。

比方说,我们需要找到在位于POINT(145.67, -30.513) 的某个用户的10,000米半径范围内是否有任何杂工,我们可以从data.py ,对数据库进行空间查询,如下所示。

from sqlalchemy.sql import func
# ..

with app.app_context():
    lng, lat = 148.523, -35.40
    geo_wkb = func.ST_PointFromWKB(User.query.all()[0].geometry)

    new_point = db.session.query(User).filter(
        func.PtDistWithin(
            User.geometry,
            geo_wkb,
            10000
        )
    ).one()
    print(new_point)

一般来说,你可能想把这些添加到你的API端点,或者把它们作为你的端点可以调用的函数,为你提供所需的信息。

总结

在本教程中,我们看到了如何设置和配置一个基本的Web应用程序,利用Spatialite和SQLite来执行对空间数据库的读写操作。

我们已经介绍了如何安装Spatiallite,还展示了如何更新你现有的SQLite实例以支持空间数据。

现在,你已经完全具备了使用Spatialite和geoalchemy2为你的项目设置开发环境的能力。