We recently started a new API project for an existing web application. The existing app has no API, and our client wants to create a fresh mobile app experience. Our new API will support the mobile app, and will exist alongside the existing web app.
The existing app is large and complex, and introducing a RESTful API that meets the mobile app's requirements would prove difficult in the existing code base. We believed that a new project for the API would be the simplest path forward. However, we need to utilize data from the existing app without impacting the existing user experience. Therefore, we decided to access the existing database from our new API project.
Our preferred tech stack uses Ruby on Rails, so we wanted to find a way to connect to the legacy database from a Rails app.
Broadly, we knew that we needed to map the existing database schema into our Rails project, capturing the schema in db/schema.rb
.
From there, we could generate model classes mapped to the schema, and then use the models to create our desired API endpoints.
Generating Model Classes
This is where the Schema to Scaffold gem came in.
It produces rails scaffold
commands that can be used to generate a model and associated controller for a given schema.
This gem rapidly speeds up the process of mapping legacy database schemas to Rails models.
To start, we needed to connect our app to a copy of the legacy database. We took a backup of our staging environment's legacy database and loaded it in our local development environment. Then we connected the rails app to the local copy of the database and ran the following command:
bundle exec rake db:schema:dump
This generated a db/schema.rb
file that we could use to generate our models.
The schema is used by the Schema to Scaffold gem to print out rails generate scaffold
commands for generating our models:
scaffold
We then copied the rails generator command list from the scaffold
command's console output to generate our models.
Depending on your use case, generating all scaffolding for each model may be excessive.
You can tweak the commands before running them to only generate the models if so desired by running rails generate model
instead of rails generate scaffold
.
Within a few moments, this leaves you with dependable ActiveRecord interfaces for accessing your legacy database. It's that easy!
Mapping Relationships
This wasn't the end of our work, however. When you're interfacing with a legacy database through a Rails app, you will likely need to make some adjustments to the generated model classes. Foreign keys or ID columns may not match the Rails naming conventions, requiring you to map the columns to the correct model attributes. We found several columns in our project that were named using camel casing instead of snake casing.
For example, this app had the concept of many types of records belonging to a Tenant
.
The database column used for this foreign key was named tenantId
instead of tenant_id
(what ActiveRecord would expect).
The Schema to Scaffold gem reasonably does not know what to do with this column, so we updated the has_one
relationships with Tenant
records to use the correct column name:
class Tenant < ApplicationRecord
has_many :filters
end
class Filter < ApplicationRecord
belongs_to :tenant, foreign_key: 'tenantId'
end
Hardcoded Relationships
We also had a special Tenant
record: a certain Tenant
was a placeholder that translated in code to "all tenants may use this".
In the old application, they'd look for this special record by a hard-coded ID of 9
.
This meant that our Rails application could not simply rely on has_many
or belongs_to
relationships to find all data that should be accessible from another Tenant
.
In some places, it made sense to use a scope instead of a has_many
relationship:
class Filter
belongs_to :tenant, foreign_key: 'tenantId'
scope :for_tenant, -> (tenant) { where('tenantId = 9 OR tenantId = ?', tenant.id) }
end
# example usage: Filter.for_tenant(tenant)
The hardcoded Tenant
with an ID of 9
also presented concerns in our test suite: the ID field is an auto-incrementing integer database column, so it's possible we could have a conflict with that ID in our tests.
To circumvent this potential issue, we seeded our test database with a placeholder Tenant
record with a hardcoded ID.
Instead of hardcoding the ID to circumvent this issue, we could have performed a database migration to adjust how to designate records as belonging to all tenants. However, this would have required adjusting behavior in the original application as well, which could have lead to undesirable side effects. We are talking about a legacy database, after all!
Going Forward
Now we have a complete integration between our legacy database and our Ruby on Rails app. The existing web application can continue working as-is with no modifications. Meanwhile, our new API endpoints can take advantage of the niceties of Rails while leveraging the shared database.