QBit: Geo Data
Geographic reference data QBit providing countries, states/provinces, and cities for QQQ applications.
Quick Start
Add the dependency to your pom.xml:
<dependency>
<groupId>com.kingsrook.qbits</groupId>
<artifactId>qbit-geo-data-core</artifactId>
<version>${qbit-geo-data.version}</version>
</dependency>
What You Get
This QBit provides three entities with pre-populated reference data:
| Entity | Fields | Records |
|---|---|---|
| Country | alpha2Code, alpha3Code, numericCode, name, officialName | 250 |
| StateProvince | countryId, code, name, subdivisionType | 5,296 |
| City | countryId, stateProvinceId, name, population, latitude, longitude | 32,000+ |
Using this QBit
Step 1: Register the QBit
In your application's metadata setup, register the QBit with a table prefix:
import com.kingsrook.qbits.geodata.GeoDataQBitConfig;
import com.kingsrook.qbits.geodata.GeoDataQBitProducer;
new GeoDataQBitProducer()
.withConfig(new GeoDataQBitConfig()
.withBackendName("rdbms")
.withTableNamePrefix("shipping"))
.produce(qInstance, "shipping-geo");
This creates three tables in your database:
shipping_countryshipping_stateProvinceshipping_city
Step 2: Run the Sync Process
The QBit includes a sync process that populates tables from bundled JSON data. Run it once after creating your tables, or schedule it to pick up updates.
import com.kingsrook.qqq.backend.core.actions.processes.RunProcessAction;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessInput;
import com.kingsrook.qbits.geodata.sync.GeoDataSyncProcessMetaDataProducer;
import com.kingsrook.qbits.geodata.sync.GeoDataSyncStep;
RunProcessInput input = new RunProcessInput();
input.setProcessName(GeoDataSyncProcessMetaDataProducer.NAME);
input.addValue(GeoDataSyncStep.FIELD_TABLE_NAME_PREFIX, "shipping");
new RunProcessAction().execute(input);
This syncs all three tables (countries, states, cities) in order.
Step 3: Query the Data
Use standard QQQ actions with the prefixed table names and the entity classes:
import com.kingsrook.qqq.backend.core.actions.tables.GetAction;
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
import com.kingsrook.qbits.geodata.model.City;
import com.kingsrook.qbits.geodata.model.Country;
import com.kingsrook.qbits.geodata.model.StateProvince;
import static com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator.*;
// Get a country by alpha-2 code
Country usa = new GetAction().executeForRecord(new GetInput("shipping_country")
.withUniqueKey(Map.of("alpha2Code", "US")), Country.class);
// Query states for a country
List<StateProvince> states = new QueryAction().execute(new QueryInput("shipping_stateProvince")
.withFilter(new QQueryFilter(new QFilterCriteria("countryId", EQUALS, usa.getId()))))
.getRecordEntities(StateProvince.class);
// Query cities with population filter
List<City> cities = new QueryAction().execute(new QueryInput("shipping_city")
.withFilter(new QQueryFilter()
.withCriteria(new QFilterCriteria("stateProvinceId", EQUALS, state.getId()))
.withCriteria(new QFilterCriteria("population", GREATER_THAN, 100000))))
.getRecordEntities(City.class);
Key concept: The table name (e.g., "shipping_country") includes your prefix. The entity class (Country.class) is always the same regardless of prefix.
Step 4: Use in Dropdowns
Each entity is registered as a PossibleValueSource for use in form dropdowns:
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
new QFieldMetaData("countryId", QFieldType.INTEGER)
.withPossibleValueSourceName("shipping_country")
Multiple Instances
Need geo data for different contexts? Register the QBit multiple times with different prefixes:
// For shipping addresses
new GeoDataQBitProducer()
.withConfig(new GeoDataQBitConfig()
.withBackendName("rdbms")
.withTableNamePrefix("shipping"))
.produce(qInstance, "shipping-geo");
// For billing addresses
new GeoDataQBitProducer()
.withConfig(new GeoDataQBitConfig()
.withBackendName("rdbms")
.withTableNamePrefix("billing"))
.produce(qInstance, "billing-geo");
This creates two independent sets of tables:
shipping_country,shipping_stateProvince,shipping_citybilling_country,billing_stateProvince,billing_city
Query each using its prefixed table name, same entity classes.
Database Schema
Generate a Liquibase changelog for your database:
import com.kingsrook.qbits.geodata.liquibase.GeoDataLiquibaseGenerator;
GeoDataQBitConfig config = new GeoDataQBitConfig()
.withTableNamePrefix("shipping")
.withEnableCountries(true)
.withEnableStateProvinces(true)
.withEnableCities(true);
GeoDataLiquibaseGenerator.generate(config, Path.of("db/changelog-geo-data.xml"));
The generator creates a changelog with your prefix substituted and only includes tables you've enabled.
License
Both Maven child modules set qbit.noticesDirectory to their parent directory,
where this repository's LICENSE and NOTICE live. This supports the shared
notice packaging correction in qbit-build-parent; it takes effect after adopting
a parent version containing that correction. Existing public parent 2.0.0 is
unchanged. Verify each generated module archive against these root files when
integrating the new parent.
Apache-2.0 - See LICENSE