Building Towards Rails
I’ve been messing with a practice app to get better at domain modeling, TDD, and the guts of Ruby and Rails. The plan was to build up an app of a few classes, give it persistence, and roll onto the web. When it came time to persist data I figured it would be easy enough to use in active record (AR) without the whole rails framework, and it is, but it can definitely be confusing.
I read and really appreciated this article on using AR in sinatra apps, and found this and and especially this very useful context. I’ll write about the initial stages of building this thing later if I’ve time.
I first built out a small flashcard application with classes for cards, decks, study sessions, and the interface. I wanted to complicate things by having cards shared between decks, mixes of cards and decks within a study session, and marking of cards with “degrees of certainty” for use with timed and spaced repetition learning techniques. That was when I knew that it was time for a database, and instead of just grabbing one and making my own ORM, I turned to active record.
Active record works just fine and dandy without rails, but it needs things to be just so. To start, get the gem and add it to your gemfile. Next, make all of your classes inherit from AR like so:
class FlashCard < ActiveRecord::Base ... end
Then you need to require it in the right places. Initially I was requiring AR from the file containing each class, but this was messy and confusing. So instead I moved to a solution that I’d seen Avi put into the CLI playlister project, which helped clean things up: I made a separate file for the entire environment called environment.rb, and made it do all of the requiring for my min-app. The environment file requires AR, points AR to the database file to use, and then requires each of the models. Then the classes don’t have to require anything.
# environment.rb # recursively requires all files in ./lib and down that end in .rb Dir.glob('./lib/*').each do |folder| Dir.glob(folder +"/*.rb").each do |file| require file end end # tells AR what db file to use ActiveRecord::Base.establish_connection( :adapter => 'sqlite3', :database => 'flash_card_app.db' )
Again, this is lovely because it lets me simply require environment.rb in everything else and then the enviroment file requires all of the models and AR, and any model that inherits from AR will work fine. (Side note: don’t expect your classes to simply work like normal after pluggin in AR. As soon as you make a class an AR child, you need to switch over to the rails Associations paradigm, remove initialize methods, and fix your getters and setters etc.)
Part of the beauty of AR is that it helps you alter, add to, and remove from databases in logical and controllable ways. Without the framework there’s no nifty rails g migration yada yada yada… So you write migrations by hand. I put my migrations in a db/migrations/ structure to mimic rails. Here’s my join-table migration:
#004_create_deck_study_sessions_table.rb require_relative '../../environment' class CreateDeckStudySessionsTable < ActiveRecord::Migration def up create_table :deck_study_sessions do |t| t.integer :deck_id t.integer :study_session_id end puts 'ran up method' end def down drop_table :deck_study_sessions puts 'ran down method' end end
It should look nearly exactly like what it would look like within the framework, except that it’s requiring that environment.rb file. Be sure to follow the AR/Rails naming conventions which I mess up all the time. For more on migrations in general and naming, check out the guide.
Requiring AR and wiring the classes and migrations is all well and good, but in order to make changes to our db we need to run the migrations. Without Rails we don’t have rake tasks for db:migrate, and my initial thought was to learn more about the way that AR applies and rolls-back migrations before diving into my own rake tasks. Seemed a bit out of scope for this little project, and perhaps one yak who’s beard I could leave alone.
Another way to run the migrations is to load everything (for me just environment.rb) into irb or pry and manually run the migrations there. It’s a bit weird but works. The argument to pass in is the method name: up, down, or change. From within irb, require environment.rb, then:
>require './db/migrations/002_create_study_sessions_table.rb' => true CreateStudySessionsTable.migrate(:up) == CreateStudySessionsTable: migrating ======================================= -- create_table(:study_sessions) -> 0.0028s == CreateStudySessionsTable: migrated (0.0029s) ============================== => nil >require './db/migrations/003_create_decks_table.rb' => true CreateDecksTable.migrate(:up) == CreateDecksTable: migrating =============================================== -- create_table(:decks) -> 0.0030s ran up method == CreateDecksTable: migrated (0.0032s) ====================================== => nil >require './db/migrations/004_create_decks_study_sessions_table.rb' => true CreateDeckStudySessionsTable.migrate(:up) == CreateDeckStudySessionsTable: migrating ================================== -- create_table(:decks_study_sessions) -> 0.0026s ran up method == CreateDeckStudySessionsTable: migrated (0.0027s) ========================= => nil
Ugly but it works. I didn’t like running these via irb (and subsequently forgetting how I ran them), so I moved the commands to migrate into the various migration files, like so:
#004_create_deck_study_sessions_table.rb require_relative '../../environment' class CreateDeckStudySessionsTable < ActiveRecord::Migration def up create_table :deck_study_sessions do |t| t.integer :deck_id t.integer :study_session_id end puts 'ran up method' end def down drop_table :deck_study_sessions puts 'ran down method' end end CreateDeckStudySessionsTable.migrate(:up)
For the moment I’m just running my migrations one at a time manually, which is definitely not the “right way” to manage them, but ok if you just want to verify that everything is connected and associated properly. (If you make a rake task or a ruby script to handle migrations, don’t forget to make sure that AR is only applying or changing the latest migration each time.)
Lastly, I wanted to be able to pass in the method to the migration when I run it from the commandline, so I changed
#004_create_deck_study_sessions_table.rb … CreateDeckStudySessionsTable.migrate(:up)
#004_create_deck_study_sessions_table.rb … CreateDeckStudySessionsTable.migrate(ARGV)
and now migrations can be run from the commandline.
1 2 3
Again, this is an fairly manual off-label fix and not suitable if you’re building an app that you expect to live with for a while. Fine in a pinch.
Filed under better late than never: I just came across an article by Wes Bailey on running migrations without rails that makes life easier and offers a much more properly programmatic way of running migrations without the whole framework, via rake tasks. Take a look: “ActiveRecord migrations without Rails”