MarkUs Blog

MarkUs Developers Blog About Their Project

Archive for the ‘activerecord’ tag

Rebooting the Schema (part 2)

with 2 comments

Last time, I talked about the old model schema, and the problems it had that lead us to refactor the code. After refactoring, this is what the database looks like:

Association

The relationships are now more concrete with the addition of memberships and assignments_groups tables. The assignments_groups is a Rails convention of declaring a many-to-many relationship between two objects by use of the join table. Thus, an assignment can have many groups, and groups can also have many assignments if a group persists throughout the course. A caveat though is to make sure that the join table is in alphabetical order, meaning it must be assignments_groups and not groups_assignments. That’s just the “convention-over-configuration” mantra of Rails at work.

Once we have the database schema set, we can then just go in and declare those relationships in the ActiveRecord classes respectively:


class Group < ActiveRecord::Base
has_and belongs_to_many :assignments
end
class Assignment < ActiveRecord::Base
has_and belongs_to_many :groups
end

However it is a different case if the join table contains extra information, which is our case with the memberships table. Here, not only does it reference the user and the group together, but it also contains extra information such as status of the member. Thus, we need to have a Membership class representing a member, and use has-many-through relationship. which sort of explicitly states that the association between a User and a Group uses memberships as its link. Here we declare the relationship as follows:

class Membership < ActiveRecord::Base
belongs_to :user
belongs_to :group
end
class User < ActiveRecord::Base
has_many :memberships
has_many :groups, :through => :memberships
end
class Group < ActiveRecord::Base
has_many :memberships
has_many :members, :through => :memberships, :source => :user
end

Abstraction

We’ve also separated the old submissions table to a submissions and submission_files tables. The new submissions table doesn’t seem to have much information and seems to be a waste of space. However, having this table allows us to delegate submission functions to a Submissions class rather than mixing them directly with either the User or Group classes. All we have to do now is just ask a User or Group for its Submission instance, and handle all queries related to submitted files from it.

Since we also want to avoid checking to see if it is a User or a Group submission everytime, we’ve abstracted the Submissions class and added separate classes for each type, UserSubmission and GroupSubmission – classes that are linked to Users and Groups respectively. Since instead of declaring the relationship with Submissions, we have:

class User < ActiveRecord::Base
has_many :submissions, :classname => UserSubmission
end
class Group < ActiveRecord::Base
has_many :submissions, :classname => GroupSubmission
end

class UserSubmission < Submission
has_many :users
end
class Group
Submission < Submission
has_many :groups
end

This allows us to call either user.submissions or group.submissions and return with an instance of the appropriate Submission subclass type.

Final Results

The refactored models with the appropriate associations gave way to a much cleaner code in the end. With the schema set in place, I’ve revisited the old code and heeded the advice in the first post, stuffing all the business logic in the appropriate models and leaving workflow control to the controllers. The result turned several functions with 200+ lines into a single function with less than 50 lines. I was also able to create more thorough unit testing while code was being written. Here, we can see that we’ve improved our stats quite a bit:

$ for f in app/controllers app/models app/helpers; do echo $f
`find $f -name "*.rb" |xargs wc -l |tail -n1`; done
app/controllers 563
app/models 591
app/helpers 60

In retrospect, I think the refactoring decisions suits us very well with what we have in mind and gives us room for modifications at the same time…until we actually start porting OLM. Stay tuned.

Written by Geofrey

October 14th, 2008 at 6:32 pm

Migrate Your Tests

without comments

Rails has a nice way of maintaining versions of your database schema through ActiveRecord:Migration.  This lets you modify your existing schema without the hassle of manually copying the same schema to all the different deployed instances that you have, and does this automatically for you.

When I usually change the schema, I usually drop all the tables and recreate them using:

rake db:migrate VERSION=0
rake db:migrate

and then repopulate my development DB environment using a script I use.  However it turns out that this doesn’t migrate your test environment automatically with it. I tried creating a simple test of making sure that my ActiveRecord validation works.  However, I ended up getting this error:

test_numericality_group_limit(AssignmentTest):
NoMethodError: undefined method `group_limit=' for #<Assignment:0xb7242018>
/var/lib/gems/1.8/gems/activerecord-2.1.0/lib/active_record/attribute_methods.rb:251:in `method_missing'
test/unit/assignment_test.rb:11:in `test_numericality_group_limit'
/var/lib/gems/1.8/gems/activesupport-2.1.0/lib/active_support/testing/setup_and_teardown.rb:33:in `__send__'
/var/lib/gems/1.8/gems/activesupport-2.1.0/lib/active_support/testing/setup_and_teardown.rb:33:in `run'

After hours of finding the bug, I found out that you have to migrate your test DB environment as well, by executing the following:

rake db:schema:dump
rake db:test:prepare

This will copy the schema that you have right now in your development environment, and copy it to your test environment.

Written by Geofrey

August 6th, 2008 at 10:49 am

Pay Attention to Little Things

with one comment

On testing:

If you ever tried running your tests and find yourself getting the error:

your_controller_test.rb:3:in `require': no such file to load -- test_helper (LoadError)
from your_controller_test.rb:3

it’s probably because the generated require call, require test_helper is not being properly recognized.  Instead of the default require 'test_helper', replace it with:

require File.dirname(__FILE__) + '/../test_helper'

If you’re doing functional testing, make sure that the controller being tested is also required (e.g. add the line require 'your_controller' on top).  Be sure that you also have installed the package libtest-unit-ruby (if using Linux), before testing.

Also, thanks to Jeff Balogh, make sure that you append test_ on all your test cases to make sure that they actually run (I know it’s obvious, but it’s nice to be reminded once in a while).

On database constraints:

If you want to enforce foreign key constraints, remember to create the required indexes first.  ActiveRecord Migration supports this using add_index.  This only applies if the column being referred to is not a primary key, if it is then there’s no need to create the index. Once this is set, you can now enforce foreign keys using ActiveRecord Migrations by implementing this.

On implementing sessions:

By default, the rails session handler stores session information on a file in the server.  This can cause scalability issues in the long run if file space runs out or when using multiple servers.  Thus, it’s recommended to use :active_record_store instead. To do this make sure that you do the following:

  • execute rake db:sessions:create to add a create table migration for sessions
  • execute rake db:migrate to apply the new schema
  • change session store to active session store by adding this line to config/environment.rb:

config.action_controller.session_store = :active_record_store

  • Make sure that you comment out or add this line to your ApplicationController:

protect_from_forgery :secret => <32_hex_digit_secret_key>

Here are also some helpful links regarding sessions and implementing it:

Written by Geofrey

July 29th, 2008 at 4:01 pm