Chapter 9. Developing Model Relationships

Everything you’ve done so far has been in the context of an application with one and only one table. That’s actually enough power to run a lot of different projects, from contact managers to time-series data collection. However, you’ll quickly find that most of the projects for which it’s worth creating a web application require more than just one table. Fortunately, Rails makes that easy, giving you the tools you need to create multiple tables and manage even complex relationships between them.

Note

If you don’t know much about databases, now is a good time to visit Appendix B. Up to this point, it’s been possible to largely forget that there was a relational database underneath the application, except for some mechanics. From this point on, you’ll need to understand how tables work in order to understand how Rails models work. (You still don’t need to understand SQL, however.)

Working with multiple tables is, on the surface, pretty simple. Every Rails model maps to a table, so working with multiple tables just means working with multiple models. The hard part is managing the relationships between the tables—which in Rails demands managing the relationships between models.

Most of the steps for working with multiple models are the same as for working with single models, just done once for each table. Once the models are created, though, the real work begins. Some of it can be done easily and declaratively, while other parts require thinking ahead and writing your own code. This chapter marks the point where Rails itself can’t directly support the operations suggested by your data models, and so there’s a lot of coding to do. While the scaffolding still provides a helpful supporting framework, there’s a lot of editing to do on models, migrations, routes, controllers, and views.

Warning

Once again, it’s important to emphasize how much easier it is to create a Rails application from scratch rather than trying to build it on top of an existing database. If you’re trying to retrofit an old database with a shiny new Rails interface, odds are good that you need a much more advanced book than this one. You’ll need to learn what goes on behind the scenes, not just how they work when all is well.

Connecting Awards to Students

The guestbook example of the previous few chapters isn’t the best foundation on which to demonstrate a multi-table application, so it’s time to change course. If you’d like to get an overview of the structures this chapter will create, these structures will be the same as those introduced in Appendix B, using students, awards, and courses. (The first version of them can be found in ch09/students001.)

Start by creating a new application:

rails students

If necessary (if you’re not in Heroku or a similar environment), cd students, and then create a student model and the usual related scaffolding:

script/generate scaffold student given_name:string middle_name:string
family_name:string date_of_birth:date grade_point_average:decimal start_date:date

Then create a second model, award, and its scaffolding:

script/generate scaffold award name:string year:integer student_id:integer

The students application now contains two models, one for students and one for awards. Students will receive awards, and awards will be connected to students, but Rails doesn’t know that yet. The script/generate command gives a hint of this because it includes a student_id field, an integer that will connect to the (unspecified but automatic) id field of the students model.

Establishing the Relationship

To tell Rails about the connections between the two models, you need to modify the models. In app/models/student.rb, add the following between the class line and the end:

# a student can have many awards
  has_many :awards

And in app/models/award.rb, add:

# every award is linked to a student, through student_id
  belongs_to :student

These two declarations establish a relationship between the two models. Student records have awards—students don’t have to have awards, but they can have many of them. Awards, however, for purposes of this example, are always linked to students.

Note

Technically, has_many and belongs_to are method names. They just happen to look like declarations, and it’s a lot easier to think of them that way.

Now Rails knows about the connections between the models. What is it going to do to support that relationship, and what’s still up to you?

Rails doesn’t add automatic checking or validation to ensure that the relationships between objects work. It doesn’t require, for example, that every award have a valid student_id. It doesn’t change the scaffolding that was already built. Establishing the connection in the model is just the first step toward building the connection into your application.

Rails does provide some help in doing that, though. With these declarations, Rails adds methods to your classes, making it much easier for a student object to work with its award objects and for an award object to work with its student objects. You can find a complete listing of the methods added in the API documentation for has_many and belongs_to. For now, it probably makes sense to show how the association can help.

Supporting the Relationship

There is only one reference to a possible connection in the original forms created by the scaffolding: a student field, meant to hold the student_id, on the forms for entering and editing awards, shown in Figure 9-1. (You’ll need to run rake db:migrate and ruby script/server to make the form appear.)

A basic awards form, where you can guess student numbers

Figure 9-1. A basic awards form, where you can guess student numbers

As it turns out, while you can enter numbers corresponding to students in the student field (if you know them, figuring them out from the URLs for student records), there isn’t any constraint on the numbers that go there. Awards can go to nonexistent students. It’s easy to improve the situation, though, by adding a select field to the app/views/awards/new.html.erb view:

<p>
    <%= f.label :student_id %><br />
    <%= f.select :student_id, Student.find(:all , :order => "family_name,
given_name").collect {|s|
      [(s.given_name + " " + s.family_name), s.id]} %>
  </p>

The highlighted piece there might seem indigestible, but it’s a fairly common way to create select lists based on related collections. The select method needs a field to bind to—:student_id—as its first parameter. The second parameter is a collection for the list to display. Student gets an object referring to the students model. The find method, which you’ve encountered before in show.html.erb templates, retrieves the list of all (:all) student records, sorted by family name and then given names (thanks to the :order parameter).

Note

Although calling Student.find(:all) works, it’s better practice for views to reference only the instance variables—i.e., the variables with names prefixed by @—rather than connecting directly to a model.

The find method doesn’t quite finish the work, though. You could stop here, if you were content to list object reference information in the select field. To show something a little more meaningful, however—both to the human user and to the program interpreting what comes back from the form—you need to specify both what gets displayed in the select field and the value that will get sent back.

That’s where the collect method is useful. It takes a block as an argument ({}). The |s| is a very brief way of saying that Ruby should loop through the collection of students and put each row into a variable named s. On each iteration of the loop, the block will return an array, contained in [ and ]. Each of those arrays, which will become lines in the select list, will have two values. The first is the name of the student, generated by concatenating its given_name to a space and its family_name. That value will be displayed to the user. The second is the id value for the student, and that value will be what comes back from the form to the server.

All of that work creates the simple form shown in Figure 9-2, with its drop-down box for students.

An awards form that minimizes guesswork about students

Figure 9-2. An awards form that minimizes guesswork about students

When the user submits this form, Rails gets back a “1” identifying the student’s id. (At least it will if the students table looks like Figure B-1 in Appendix B.) That will go in the student_id field in the table. A “1” will be puzzling for humans, though. To fix that, in app/views/awards/, in show.html.erb, and index.html.erb, you might want to replace:

<%=h @award.student_id %>

with:

<%=h @award.student.given_name %> <%=h @award.student.family_name %>

Note that the @award variable (just award in index.html.erb) suddenly has a new method. Of course it understood student_id—that’s a field defined by the original script/generate command. But the student method, and its methods given_name and family_name, are new. Those features are the result of Rails recognizing the belong_to declaration and providing a more convenient notation for getting to the specific student that this particular award belongs_to.

While using student is great, there’s one problem with the code just shown—it keeps repeating itself to combine given_name and family_name. There’s a way to avoid that and to simplify most of this code. In the model for student (in app/models.student.rb), add a method called name that returns a simpler form:

def name
  given_name + " " + family_name
end

Like the methods representing database fields, the name method will be available from awards, as in:

<%=h @award.student.name %>

or:

<%= f.select :student_id, Student.find(:all).collect {|s|
      [s.name, s.id]} %>

You’ll now get the cleaner-looking result shown in Figure 9-3 for a little less work.

Showing a record with a name instead of a student ID number

Figure 9-3. Showing a record with a name instead of a student ID number

Awards are now connected to students, but there still isn’t any enforcement of that connection, just a form field that makes it difficult to enter anything else. Even with the form, though, there are corner cases—someone could, for example, delete a student after the form had been sent to a user. Or, more likely, a REST request could send XML with a bad student_id—fixing up the view hasn’t changed anything in the model.

Guaranteeing a Relationship

Rails itself doesn’t provide a simple mechanism for validating that the student_id matches a student. You could, if you’re handy with the underlying database, add such constraints through migrations. If you’d rather do something that feels like it’s Rails, however, and operates within the model instead of the database, you can install the validates_existence_of plug-in. From your application’s top directory, issue the command:

ruby script/plugin install
http://svn.hasmanythrough.com/public/plugins/validates_existence/

and add this line underneath the belongs_to declaration of app/models/awards.rb:

validates_existence_of :student

Now, if you try to save an award record with a student that doesn’t exist, you’ll get a message like that shown in Figure 9-4. (Note that because the student was deleted, his or her name isn’t available in the select list, and Giles Boschwick comes up again.)

Enforcing the existence of students for every award

Figure 9-4. Enforcing the existence of students for every award

If you don’t check for the existence of the student, then users will see a strange and incomprehensible message about a nil object when the view tries to process award.student.name, so this is most likely an improvement. For more information on validates_existence_of, including discussion on whether it’s a good idea in the first place, see http://blog.hasmanythrough.com/2007/7/14/validate-your-existence.

Note

As that blog entry notes, you could decide to use Rails’ built-in validates_associated method for this purpose, but it goes beyond checking whether there is an associated record all the way into checking whether there is a valid associated record. Depending on your needs, that could be more appropriate, but validates_existence_of is lighter-weight.

Later in this chapter, you’ll see another approach to connecting awards to students that helps avoid these problems: nested resources.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset