Skip to content

Commit ef1ce39

Browse files
author
Éric Lemoine
committed
Extend docs for SpatiaLite
1 parent b9b03d8 commit ef1ce39

File tree

3 files changed

+248
-10
lines changed

3 files changed

+248
-10
lines changed

TEST.rst

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,17 @@ Install the Python and PostgreSQL development packages::
1313

1414
$ sudo apt-get install python2.7-dev libpq-dev libgeos-dev
1515

16+
Install SpatiaLite::
17+
18+
$ sudo apt-get install libsqlite3-mod-spatialite
19+
1620
Install the Python dependencies::
1721

1822
$ pip install -r requirements.txt
1923
$ pip install psycopg2
2024

21-
Set up the database
22-
===================
25+
Set up the PostGIS database
26+
===========================
2327

2428
Create the ``gis`` role::
2529

@@ -41,7 +45,18 @@ For PostGIS 1.5::
4145

4246
For PostGIS 2::
4347

44-
$sudo -u postgres psql -d gis -U postgres -c "CREATE EXTENSION postgis;"
48+
$ sudo -u postgres psql -d gis -U postgres -c "CREATE EXTENSION postgis;"
49+
50+
Set the path to the SpatiaLite module
51+
=====================================
52+
53+
By default the SpatiaLite functional tests are not run. To run them the ``SPATIALITE_LIBRARY_PATH``
54+
environment variable must be set.
55+
56+
For example, on Debian Sid, and relying on the official SpatiaLite Debian package, the path to
57+
the SpatiaLite library is ``/usr/lib/x86_64-linux-gnu/mod_spatialite.so``, so you would use this::
58+
59+
$ export SPATIALITE_LIBRARY_PATH="/usr/lib/x86_64-linux-gnu/mod_spatialite.so"
4560

4661
Run Tests
4762
=========

doc/index.rst

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,8 @@ working with spatial databases.
99
GeoAlchemy 2 focuses on `PostGIS <http://postgis.net/>`_. PostGIS 1.5 and
1010
PostGIS 2 are supported.
1111

12-
.. note::
13-
14-
GeoAlchemy 2 doesn't currently support other dialects than
15-
PostgreSQL/PostGIS. Supporting Oracle Locator in the previous series was
16-
the main contributor to code complexity. So it is currently not clear
17-
whether we want to go there again.
12+
SpatiaLite is also supported, but using GeoAlchemy 2 with SpatiaLite requires some specific
13+
configuration on the application side. GeoAlchemy 2 works with SpatiaLite 4.3.0 and higher.
1814

1915
GeoAlchemy 2 aims to be simpler than its predecessor, `GeoAlchemy
2016
<https://pypi.python.org/pypi/GeoAlchemy>`_. Simpler to use, and simpler
@@ -41,7 +37,6 @@ What's New in GeoAlchemy 2
4137

4238
* GeoAlchemy 2 supports PostGIS' ``geometry`` type, as well as the ``geography``
4339
and ``raster`` types.
44-
4540
* The first series had its own namespace for spatial functions. With GeoAlchemy
4641
2, spatial functions are called like any other SQLAlchemy function, using
4742
``func``, which is SQLAlchemy's `standard way
@@ -78,6 +73,7 @@ system. If you're new to GeoAlchemy 2 start with this.
7873

7974
orm_tutorial
8075
core_tutorial
76+
spatialite_tutorial
8177

8278
Reference Documentation
8379
-----------------------

doc/spatialite_tutorial.rst

Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
1+
.. _spatialite_tutorial:
2+
3+
SpatiaLite Tutorial
4+
===================
5+
6+
GeoAlchemy 2's main target is PostGIS. But GeoAlchemy 2 also supports SpatiaLite, the spatial
7+
extension to SQLite. This tutorial describes how to use GeoAlchemy 2 with SpatiaLite. It's based on
8+
the :ref:`orm_tutorial`, which you may want to read first.
9+
10+
Connect to the DB
11+
-----------------
12+
13+
Just like when using PostGIS connecting to a SpatiaLite database requires an ``Engine``. This is how
14+
you create one for SpatiaLite::
15+
16+
>>> from sqlalchemy import create_engine
17+
>>> from sqlalchemy.event import listen
18+
>>>
19+
>>> def load_spatialite(dbapi_conn, connection_record):
20+
... dbapi_conn.enable_load_extension(True)
21+
... dbapi_conn.load_extension('/usr/lib/x86_64-linux-gnu/mod_spatialite.so')
22+
...
23+
>>>
24+
>>> engine = create_engine('sqlite:///gis.db', echo=True)
25+
>>> listen(engine, 'connect', load_spatialite)
26+
27+
The call to ``create_engine`` creates an engine bound to the database file ``gis.db``. After that
28+
a ``connect`` listener is registered on the engine. The listener is responsible for loading the
29+
SpatiaLite extension, which is a necessary operation for using SpatiaLite through SQL.
30+
31+
At this point you can test that you are able to connect to the database::
32+
33+
>> conn = engine.connect()
34+
2018-05-30 17:12:02,675 INFO sqlalchemy.engine.base.Engine SELECT CAST('test plain returns' AS VARCHAR(60)) AS anon_1
35+
2018-05-30 17:12:02,676 INFO sqlalchemy.engine.base.Engine ()
36+
2018-05-30 17:12:02,676 INFO sqlalchemy.engine.base.Engine SELECT CAST('test unicode returns' AS VARCHAR(60)) AS anon_1
37+
2018-05-30 17:12:02,676 INFO sqlalchemy.engine.base.Engine ()
38+
39+
You can also check that the ``gis.db`` SQLite database file was created on the file system.
40+
41+
One additional step is required for using SpatiaLite: create the ``geometry_columns`` and
42+
``spatial_ref_sys`` metadata tables. This is done by calling SpatiaLite's ``InitSpatialMetaData``
43+
function::
44+
45+
>>> from sqlalchemy.sql import select, func
46+
>>>
47+
>>> conn.execute(select([func.InitSpatialMetaData()]))
48+
49+
Note that this operation may take some time the first time it is executed for a database. When
50+
``InitSpatialMetaData`` is executed again it will report an error::
51+
52+
InitSpatiaMetaData() error:"table spatial_ref_sys already exists"
53+
54+
You can safely ignore that error.
55+
56+
Before going further we can close the current connection::
57+
58+
>>> conn.close()
59+
60+
Declare a Mapping
61+
-----------------
62+
63+
Now that we have a working connection we can go ahead and create a mapping between
64+
a Python class and a database table.
65+
66+
::
67+
68+
>>> from sqlalchemy.ext.declarative import declarative_base
69+
>>> from sqlalchemy import Column, Integer, String
70+
>>> from geoalchemy2 import Geometry
71+
>>>
72+
>>> Base = declarative_base()
73+
>>>
74+
>>> class Lake(Base):
75+
... __tablename__ = 'lake'
76+
... id = Column(Integer, primary_key=True)
77+
... name = Column(String)
78+
... geom = Column(Geometry(geometry_type='POLYGON', management=True, use_st_prefix=False))
79+
80+
This basically works in the way as with PostGIS. The difference is the ``management`` and
81+
``use_st_prefix`` arguments that must be set to ``True`` and ``False``, respectively.
82+
83+
Setting ``management`` to ``True`` indicates that the ``AddGeometryColumn`` and
84+
``DiscardGeometryColumn`` management functions will be used for the creation and removal of the
85+
geometry column. This is required with SpatiaLite.
86+
87+
Setting ``use_st_prefix`` to ``False`` indicates that ``GeomFromEWKT`` and ``AsEWKB`` will be used
88+
rather than ``ST_GeomFromEWKT`` and ``ST_AsEWKB``. Again this is required with SpatiaLite, as
89+
SpatiaLite doesn't have ``ST_GeomFromEWKT`` and ``ST_AsEWKB`` functions.
90+
91+
Create the Table in the Database
92+
--------------------------------
93+
94+
We can now create the ``lake`` table in the ``gis.db`` database::
95+
96+
>>> Lake.__table__.create(engine)
97+
98+
If we wanted to drop the table we'd use::
99+
100+
>>> Lake.__table__.drop(engine)
101+
102+
There's nothing specific to SpatiaLite here.
103+
104+
Create a Session
105+
----------------
106+
107+
When using the SQLAlchemy ORM the ORM interacts with the database through a ``Session``.
108+
109+
>>> from sqlalchemy.orm import sessionmaker
110+
>>> Session = sessionmaker(bind=engine)
111+
>>> session = Session()
112+
113+
The session is associated with our SpatiaLite ``Engine``. Again, there's nothing
114+
specific to SpatiaLite here.
115+
116+
Add New Objects
117+
---------------
118+
119+
We can now create and insert new ``Lake`` objects into the database, the same way we'd
120+
do it using GeoAlchemy 2 with PostGIS.
121+
122+
::
123+
124+
>>> lake = Lake(name='Majeur', geom='POLYGON((0 0,1 0,1 1,0 1,0 0))')
125+
>>> session.add(lake)
126+
>>> session.commit()
127+
128+
We can now query the database for ``Majeur``::
129+
130+
>>> our_lake = session.query(Lake).filter_by(name='Majeur').first()
131+
>>> our_lake.name
132+
u'Majeur'
133+
>>> our_lake.geom
134+
<WKBElement at 0x9af594c; '0103000000010000000500000000000000000000000000000000000000000000000000f03f0000000000000000000000000000f03f000000000000f03f0000000000000000000000000000f03f00000000000000000000000000000000'>
135+
>>> our_lake.id
136+
1
137+
138+
Let's add more lakes::
139+
140+
>>> session.add_all([
141+
... Lake(name='Garde', geom='POLYGON((1 0,3 0,3 2,1 2,1 0))'),
142+
... Lake(name='Orta', geom='POLYGON((3 0,6 0,6 3,3 3,3 0))')
143+
... ])
144+
>>> session.commit()
145+
146+
Query
147+
-----
148+
149+
Let's make a simple, non-spatial, query::
150+
151+
>>> query = session.query(Lake).order_by(Lake.name)
152+
>>> for lake in query:
153+
... print(lake.name)
154+
...
155+
Garde
156+
Majeur
157+
Orta
158+
159+
Now a spatial query::
160+
161+
>>> from geolachemy2 import WKTElement
162+
>>> query = session.query(Lake).filter(
163+
... func.ST_Contains(Lake.geom, WKTElement('POINT(4 1)')))
164+
...
165+
>>> for lake in query:
166+
... print(lake.name)
167+
...
168+
Orta
169+
170+
Here's another spatial query, using ``ST_Intersects`` this time::
171+
172+
>>> query = session.query(Lake).filter(
173+
... Lake.geom.ST_Intersects(WKTElement('LINESTRING(2 1,4 1)')))
174+
...
175+
>>> for lake in query:
176+
... print(lake.name)
177+
...
178+
Garde
179+
Orta
180+
181+
We can also apply relationship functions to :class:`geoalchemy2.elements.WKBElement`. For example::
182+
183+
>>> lake = session.query(Lake).filter_by(name='Garde').one()
184+
>>> print(session.scalar(lake.geom.ST_Intersects(WKTElement('LINESTRING(2 1,4 1)'))))
185+
1
186+
187+
``session.scalar`` allows executing a clause and returning a scalar value (an integer value in this
188+
case).
189+
190+
The value ``1`` indicates that the lake "Garde" does intersects the ``LINESTRING(2 1,4 1)``
191+
geometry. See the SpatiaLite SQL functions reference list for more information.
192+
193+
Caveats
194+
-------
195+
196+
You may encounter cases where queries will fail with the following error::
197+
198+
sqlalchemy.exc.OperationalError: (sqlite3.OperationalError) no such function: ST_AsEWKB [SQL:...
199+
200+
For example the following query will produce this error::
201+
202+
>>> buffers = session.query(Lake.geom.ST_Buffer(2)).all()
203+
204+
The query fails because GeoAlchemy 2 sets :class:`geoalchemy2.types.Geometry` as the return type
205+
of ``ST_Buffer``, but ``use_st_prefix`` defaults to ``True`` in the ``Geometry`` class. To work
206+
around the issue it is required to pass a properly configured ``Geometry`` instance when calling
207+
``ST_Buffer``::
208+
209+
>>> geometry_type = Geometry(management=True, use_st_prefix=False)
210+
>>> buffers = session.query(Lake.geom.ST_Buffer(2, type_=geometry_type)
211+
212+
This issue applies to all the functions that return geometries: ``ST_Buffer``, ``ST_Difference``,
213+
``ST_Intersection``, etc.
214+
215+
Here is another example where passing a ``type_`` is required::
216+
217+
>>> lake = session.query(Lake).filter_by(name='Garde').one()
218+
>>> lake_buffer = session.scalar(lake.geom.ST_Buffer(2, type_=geometry_type)
219+
220+
Further Reference
221+
-----------------
222+
223+
* GeoAlchemy 2 ORM Tutotial: :ref:`orm_tutorial`
224+
* GeoAlchemy 2 Spatial Functions Reference: :ref:`spatial_functions`
225+
* GeoAlchemy 2 Spatial Operators Reference: :ref:`spatial_operators`
226+
* GeoAlchemy 2 Elements Reference: :ref:`elements`
227+
* `SpatiaLite 4.3.0 SQL functions reference list <http://www.gaia-gis.it/gaia-sins/spatialite-sql-4.3.0.html>`_

0 commit comments

Comments
 (0)