Lesson 2 — Your first geometry column
In this lesson we add the dispatch app’s first location-aware
model: a Depot. Depots are the regional dispatch hubs — the centres that
coordinate field work across a region. Field officers don’t
typically work out of the depot in person; they’re spread
across the region the depot manages. Western Australia is the
extreme case: a single depot in Perth coordinates field work
across an area larger than most countries. Each depot has a
location — the actual building where coordinators sit and
dispatchers run their shifts.
Adding the model is mostly conventional Rails. The interesting
part is the location column, which won’t be a pair of decimal
columns or a JSON blob. It’ll be a real PostGIS geometry
column, and ActiveRecord will treat it as a first-class spatial
attribute — automatically converting between the database’s
binary representation and Ruby objects we can call methods on.
By the end of the lesson the database will have the first depot — Sydney’s central warehouse — and you’ll be able to query it back several ways.
Generating the model
Generate the Depot model. We’ll add the spatial column
manually after generating, so the standard generator command
is sufficient:
|
|
This produces a migration file at
db/migrate/<timestamp>_create_depots.rb and an empty Depot
model at app/models/depot.rb.
Adding the geometry column
Open the generated migration. The default file looks like this:
|
|
Edit it so it reads:
|
|
Three substantive changes from the generator’s default:
t.st_point :location, srid: 4326 — declares a PostGIS
geometry(Point, 4326) column. The st_point type
constrains the column to point geometries (rejecting
linestrings or polygons that might end up there by accident).
srid: 4326 specifies the coordinate system as WGS84 lat/lng
— the same system we discussed in Lesson 1.
null: false on each column. Depots without a name, code,
or location wouldn’t be useful to anyone, so we reject them at
the schema level.
add_index :depots, :location, using: :gist — adds a GiST
index on the geometry column. GiST is PostGIS’s spatial index
type. We won’t use the index meaningfully until Module 7 (where
it becomes essential for performance), but the discipline of
indexing geometry columns at creation time is worth following
from the start. The seatbelt analogy applies — you add it
without thinking, and it’s there if you need it.
Run the migration:
|
|
PostgreSQL output should indicate that depots was created.
Now look at db/schema.rb and find the new entry. The
location column appears as a geometry column with the SRID
and type constraint recorded:
|
|
The limit: hash is how the postgis adapter records spatial
metadata in schema.rb. Compare with the code index above
it — there’s no using: parameter on a regular B-tree index;
the postgis adapter remembers we asked for :gist and emits
it on round-trip.
Inserting the first depot
Let’s put a depot in the database. We’ll use raw SQL for this
first insertion because it makes the spatial value visible —
using ActiveRecord directly would hide what’s happening with
location.
Open psql:
|
|
Then:
|
|
Two PostGIS function calls build the location value.
ST_MakePoint(longitude, latitude) constructs a point.
ST_SetSRID(geometry, 4326) tags the point with SRID 4326 so
the column constraint accepts it. Without ST_SetSRID, the
point would be created with SRID 0 (no spatial reference) and
the insert would fail because the column requires SRID 4326.
Notice the coordinate order: longitude first (151.2093), then
latitude (-33.8688). PostGIS uses [lng, lat] natively, like
GeoJSON and MapLibre. We saw this convention in Module 2
Lesson 4.
PostgreSQL should respond INSERT 0 1 indicating one row was
inserted.
Querying the depot back
Three ways to look at the row.
See the binary form — the WKB hex string we met in Lesson 1:
|
|
Long, unreadable, the database’s internal representation.
See the WKT form — readable text:
|
|
|
|
See the GeoJSON form — what we’ll eventually serve to the browser:
|
|
|
|
Module 4 shows how a Rails controller produces the GeoJSON form and how a map component consumes it.
Query the depot’s coordinates as scalars
The ST_X and ST_Y accessor functions return a point’s
longitude and latitude as plain numbers. In psql:
|
|
You should see:
|
|
ST_X returns the X coordinate (longitude in lat/lng
systems). ST_Y returns Y (latitude).
For meaningful spatial work, you don’t pull out X and Y; you use spatial functions that operate on geometries directly (point-in-polygon, distance, intersects, and so on). But the accessors are useful for inspection — and occasionally for building queries that combine spatial and non-spatial filtering.
Querying via ActiveRecord
The ActiveRecord side is where the
activerecord-postgis-adapter earns its keep. Open a Rails
console:
|
|
|
|
The location attribute comes back as an RGeo geometry
object — a Ruby object representing the point. RGeo is the
Ruby spatial library that the adapter uses to wrap geometry
values. RGeo objects have methods that mirror PostGIS
functions:
|
|
The adapter handles WKB ⇄ RGeo object conversion automatically. You write Ruby; the adapter speaks PostGIS.
Inserting from Ruby is similarly transparent. The simplest way is to assign a WKT string and let the adapter parse it. In the Rails console, add a Brisbane depot:
|
|
The adapter parses the WKT, converts it to an RGeo geometry, and serialises it to WKB on the way to the database. No SRID needed in the WKT string because the column constraint forces 4326.
Verify it’s there:
|
|
This is the point at which the WKT-vs-Ruby boundary disappears
into ActiveRecord and you can mostly stop thinking about it.
The location attribute behaves like any other column from
the consumer’s perspective — set on creation, read back later
— except that the value is a structured spatial object rather
than a string or number.
A note on coordinate ordering, again
You’ll notice the insertion forms use longitude first, like
PostGIS, GeoJSON, and MapLibre. WKT’s POINT(x y) means
POINT(longitude latitude) for lat/lng data.
The Vera gem’s Ruby DSL is the exception that flips to latitude first because that’s the Ruby-natural convention. But once you cross the boundary into spatial data — WKT, GeoJSON, the database — coordinate order is longitude-first, consistently.
This division — Ruby DSL is [lat, lng], spatial data is
[lng, lat] — is a deliberate choice. It puts the natural
order in the surface developers write the most, and the
standards-compliant order at the wire level where consistency
matters. Know which side of the boundary you’re on.
Where this leaves us
The depots table now exists with two real rows. Each row has
a typed point geometry constrained to SRID 4326, with a GiST
index ready to make spatial queries fast when we have enough
data to need it.
You’ve seen:
- The
t.st_pointmigration syntax for a typed geometry column. - The
using: :gistsyntax for a spatial index. - WKB, WKT, and GeoJSON as three representations of the same geometry value.
- ActiveRecord’s automatic conversion to RGeo geometry objects.
- The WKT-string pattern for setting a geometry attribute from Ruby.
In Lesson 3 we go deeper into SRIDs — what they actually are, what 4326 represents under the hood, and what other SRIDs you might encounter. Lesson 1 gave you enough to follow along; the next lesson fills in the picture.