Files
thrilltrack-explorer/django-backend/POSTGIS_SETUP.md

8.6 KiB

PostGIS Integration - Dual-Mode Setup

Overview

ThrillWiki Django backend uses a conditional PostGIS setup that allows geographic data to work in both local development (SQLite) and production (PostgreSQL with PostGIS).

How It Works

Database Backends

  • Local Development: Uses regular SQLite without GIS extensions

    • Geographic coordinates stored in latitude and longitude DecimalFields
    • No spatial query capabilities
    • Simpler setup, easier for local development
  • Production: Uses PostgreSQL with PostGIS extension

    • Geographic coordinates stored in location_point PointField (PostGIS)
    • Full spatial query capabilities (distance calculations, geographic searches, etc.)
    • Automatically syncs with legacy latitude/longitude fields

Model Implementation

The Park model uses conditional field definition:

# Conditionally import GIS models only if using PostGIS backend
_using_postgis = (
    'postgis' in settings.DATABASES['default']['ENGINE']
)

if _using_postgis:
    from django.contrib.gis.db import models as gis_models
    from django.contrib.gis.geos import Point

Fields in SQLite mode:

  • latitude (DecimalField) - Primary coordinate storage
  • longitude (DecimalField) - Primary coordinate storage

Fields in PostGIS mode:

  • location_point (PointField) - Primary coordinate storage with GIS capabilities
  • latitude (DecimalField) - Deprecated, kept for backward compatibility
  • longitude (DecimalField) - Deprecated, kept for backward compatibility

Helper Methods

The Park model provides methods that work in both modes:

set_location(longitude, latitude)

Sets park location from coordinates. Works in both modes:

  • SQLite: Updates latitude/longitude fields
  • PostGIS: Updates location_point and syncs to latitude/longitude
park.set_location(-118.2437, 34.0522)

coordinates property

Returns coordinates as (longitude, latitude) tuple:

  • SQLite: Returns from latitude/longitude fields
  • PostGIS: Returns from location_point (falls back to lat/lng if not set)
coords = park.coordinates  # (-118.2437, 34.0522)

latitude_value property

Returns latitude value:

  • SQLite: Returns from latitude field
  • PostGIS: Returns from location_point.y

longitude_value property

Returns longitude value:

  • SQLite: Returns from longitude field
  • PostGIS: Returns from location_point.x

Setup Instructions

Local Development (SQLite)

  1. No special setup required! Just use the standard SQLite database:

    # django/config/settings/local.py
    DATABASES = {
        'default': {
            'ENGINE': 'django.db.backends.sqlite3',
            'NAME': BASE_DIR / 'db.sqlite3',
        }
    }
    
  2. Run migrations as normal:

    python manage.py migrate
    
  3. Use latitude/longitude fields for coordinates:

    park = Park.objects.create(
        name="Test Park",
        latitude=40.7128,
        longitude=-74.0060
    )
    

Production (PostgreSQL with PostGIS)

  1. Install PostGIS extension in PostgreSQL:

    CREATE EXTENSION postgis;
    
  2. Configure production settings:

    # django/config/settings/production.py
    DATABASES = {
        'default': {
            'ENGINE': 'django.contrib.gis.db.backends.postgis',
            'NAME': 'thrillwiki',
            'USER': 'your_user',
            'PASSWORD': 'your_password',
            'HOST': 'your_host',
            'PORT': '5432',
        }
    }
    
  3. Run migrations:

    python manage.py migrate
    

    This will create the location_point PointField in addition to the latitude/longitude fields.

  4. Use location_point for geographic queries:

    from django.contrib.gis.geos import Point
    from django.contrib.gis.measure import D
    
    # Create park with PostGIS Point
    park = Park.objects.create(
        name="Test Park",
        location_point=Point(-118.2437, 34.0522, srid=4326)
    )
    
    # Geographic queries (only in PostGIS mode)
    nearby_parks = Park.objects.filter(
        location_point__distance_lte=(
            Point(-118.2500, 34.0500, srid=4326),
            D(km=10)
        )
    )
    

Migration Strategy

From SQLite to PostgreSQL

When migrating from local development (SQLite) to production (PostgreSQL):

  1. Export your data from SQLite
  2. Set up PostgreSQL with PostGIS
  3. Run migrations (will create location_point field)
  4. Import your data (latitude/longitude fields will be populated)
  5. Run a data migration to populate location_point from lat/lng:
# Example data migration
from django.contrib.gis.geos import Point

for park in Park.objects.filter(latitude__isnull=False, longitude__isnull=False):
    if not park.location_point:
        park.location_point = Point(
            float(park.longitude),
            float(park.latitude),
            srid=4326
        )
        park.save(update_fields=['location_point'])

Benefits

  1. Easy Local Development: No need to install PostGIS or SpatiaLite for local development
  2. Production Power: Full GIS capabilities in production with PostGIS
  3. Backward Compatible: Keeps latitude/longitude fields for compatibility
  4. Unified API: Helper methods work the same in both modes
  5. Gradual Migration: Can migrate from SQLite to PostGIS without data loss

Limitations

In SQLite Mode (Local Development)

  • No spatial queries: Cannot use PostGIS query features like:

    • distance_lte, distance_gte (distance-based searches)
    • dwithin (within distance)
    • contains, intersects (geometric operations)
    • Geographic indexing for performance
  • Workarounds for local development:

    • Use simple filters on latitude/longitude ranges
    • Implement basic distance calculations in Python if needed
    • Most development work doesn't require spatial queries

In PostGIS Mode (Production)

  • Use location_point for queries: Always use the location_point field for geographic queries, not lat/lng
  • Sync fields: If updating location_point directly, remember to sync to lat/lng if needed for compatibility

Testing

Test in SQLite (Local)

cd django
python manage.py shell

# Test basic CRUD
from apps.entities.models import Park
from decimal import Decimal

park = Park.objects.create(
    name="Test Park",
    park_type="theme_park",
    latitude=Decimal("40.7128"),
    longitude=Decimal("-74.0060")
)

print(park.coordinates)  # Should work
print(park.latitude_value)  # Should work

Test in PostGIS (Production)

cd django
python manage.py shell

# Test GIS features
from apps.entities.models import Park
from django.contrib.gis.geos import Point
from django.contrib.gis.measure import D

park = Park.objects.create(
    name="Test Park",
    park_type="theme_park",
    location_point=Point(-118.2437, 34.0522, srid=4326)
)

# Test distance query
nearby = Park.objects.filter(
    location_point__distance_lte=(
        Point(-118.2500, 34.0500, srid=4326),
        D(km=10)
    )
)

Future Considerations

  1. Remove Legacy Fields: Once fully migrated to PostGIS in production and all code uses location_point, the latitude/longitude fields can be deprecated and eventually removed

  2. Add Spatial Indexes: In production, add spatial indexes for better query performance:

    class Meta:
        indexes = [
            models.Index(fields=['location_point']),  # Spatial index
        ]
    
  3. Geographic Search API: Build geographic search endpoints that work differently based on backend:

    • SQLite: Simple bounding box searches
    • PostGIS: Advanced spatial queries with distance calculations

Troubleshooting

"AttributeError: 'DatabaseOperations' object has no attribute 'geo_db_type'"

This error occurs when trying to use PostGIS PointField with regular SQLite. Solution:

  • Ensure you're using the local.py settings which uses regular SQLite
  • Make sure migrations were created with SQLite active (no location_point field)

"No such column: location_point"

This occurs when:

  • Code tries to access location_point in SQLite mode
  • Solution: Use the helper methods (coordinates, latitude_value, longitude_value) instead

"GDAL library not found"

This occurs when django.contrib.gis is loaded but GDAL is not installed:

  • Even with SQLite, GDAL libraries must be available because django.contrib.gis is in INSTALLED_APPS
  • Install GDAL via Homebrew: brew install gdal geos
  • Configure paths in settings if needed

References