Report INFLECTOR Bugs to the Core Team?

Một phần của tài liệu The rails way (Trang 217 - 370)

According to Michael Koziarski, one of the members of the Rails core team, you shouldn’t report issues with Inflector: “The inflector is basically frozen, prior to 1.0 we’d add lots of new rules to fix bugs, and just end up enraging people who looked at the old output and named their tables accordingly. You can add those exceptions your- self in environment.rb.”

Setting Names Manually

Now that we understand inflection, let’s get back to configuration of ActiveRecord model classes. The set_table_nameandset_primary_keymethods let you bypass Rails conventions and explicitly define the table name for the model, and the column name for its primary key.

For example purposes (only!), let’s say I had some icky naming convention that I was forced to follow for my database tables, that differed from ActiveRecord’s con- vention. I might have to do the following:

class Client < ActiveRecord::Base set_table_name “CLIENT”

set_primary_key “CLIENT_ID”

The set_table_name and set_primary_key methods let you use any table and primary names you’d like, but you’ll have to specify them explicitly in your model class. It’s only a couple of extra lines per model, but on a large application it adds unnecessary complexity, so don’t do it if you don’t absolutely have to.

When you’re not at liberty to dictate the naming guidelines for your database schema, such as when a separate DBA group controls all database schemas, then you probably don’t have a choice. But if you have flexibility, you should really just follow Rails’ conventions. They might not be what you’re used to, but following them will save you time and unnecessary headaches.

Legacy Naming Schemes

If you are working with legacy schemas, you may be tempted to automatically

set_table_nameeverywhere, whether you need it or not. Before you get accustomed to doing that, learn the additional options available that might just be more DRY and make your life easier.

Let’s assume you need to turn off table pluralization altogether; you would set the following attribute to false at the bottom of config/environment.rb:

ActiveRecord::Base.pluralize_table_names = false

There are various other useful attributes of ActiveRecord::Base, provided for configuring Rails to work with legacy naming schemes.

• primary_key_prefix_type Accessor for the prefix type that will be prepend- ed to every primary key column name. If :table_name is specified, the ActiveRecord will look for “tableid” instead of “id” as the primary column. If

:table_name_with_underscore is specified, ActiveRecord will look for

“table_id” instead of “id”.

• table_name_prefix Some departments prefix table names with the name of the database. Set this attribute accordingly to avoid having to include the prefix in all of your model class names.

• table_name_suffix Similar to prefix, but adds a common ending to all table names.

• underscore_table_names Set to false to prevent ActiveRecord from underscoring compound table names.

Defining Attributes

The list of attributes associated with an ActiveRecord model class is not coded explic- itly. At runtime, the ActiveRecord model examines the database schema directly from the server. Adding, removing, and changing attributes and their type is done by manipulating the database itself, either directly using SQL commands or GUI tools, but ideally via ActiveRecord migrations.

The practical implication of the ActiveRecord pattern is that you have to define your database table structure and make sure it actually exists in the database prior to working with your persistent models. Some people may have issues with that design philosophy, especially if they’re coming from a background in top-down design.

The Rails way is undoubtedly to have model classes that map closely to your data- base schema. On the other hand, remember you can have models that are simple Ruby classes and do not extend ActiveRecord::Base. Among other things, it is common to use non-ActiveRecord model classes to encapsulate data and logic for the view layer.

Default Attribute Values

Migrations let you define default attribute values by passing a :defaultoption to the

columnmethod, but most of the time you’ll want to set default attribute values at the model layer, not the database layer. Default values are part of your domain logic and should be kept together with the rest of the domain logic of your application, in the model layer.

A common example is the case when your model should return the string ‘n/a’

instead of a nil (or empty) string for an attribute that has not been populated yet.

Seems simple enough and it’s a good place to start looking into how attributes exist at runtime.

To begin, let’s whip up a quick test case describing the desired behavior.

class SpecificationTest < Test::Unit::TestCase def test_default_string_for_tolerance_should_be_na

spec = Specification.new

assert_equal ‘n/a’, spec.tolerance end

end

We run that test and it fails, as expected. ActiveRecord doesn’t provide us with any class-level methods to define default values for models declaratively. So it seems we’ll

Normally, attribute accessors are handled magically by ActiveRecord’s internals, but in this case we’re overriding the magic with an explicit getter. All we need to do is to define a method with the same name as the attribute and use Ruby’s oroperator, which will short-circuit if @toleranceis not nil.

class Specification < ActiveRecord::Base def tolerance

@tolerance or ‘n/a’

end end

Now we run the test and it passes. Great. Are we done? Not quite. We should test a case when the real tolerance value should be returned. I’ll add another test for a spec- ification with a not-nil tolerance value and also go ahead and make my test method names a little more descriptive.

class SpecificationTest < Test::Unit::TestCase

def test_default_string_for_tolerance_should_return_na_when_nil spec = Specification.new

assert_equal ‘n/a’, spec.tolerance end

def test_tolerance_value_should_be_returned_when_not_nil spec = Specification.new(:tolerance => ‘0.01mm’) assert_equal ‘0.01mm’, spec.tolerance

end end

Uh-oh. The second test fails. Seems our default ‘n/a’ string is being returned no matter what. That means that @tolerancemust not get set. Should we even know that it is getting set or not? It is an implementation detail of ActiveRecord, is it not?

The fact that Rails does not use instance variables like @toleranceto store the model attributes is in fact an implementation detail. But model instances have a cou- ple of methods, write_attribute and read_attribute, conveniently provided by ActiveRecord for the purposes of overriding default accessors, which is exactly what we’re trying to do. Let’s fix our Specification class.

class Specification < ActiveRecord::Base def tolerance

read_attribute(:tolerance) or ‘n/a’

Now the test passes. How about a simple example of using write_attribute?

class SillyFortuneCookie < ActiveRecord::Base def message=(txt)

write_attribute(:message, txt + ‘ in bed’) end

end

Alternatively, both of these examples could have been written with the shorter forms of reading and writing attributes, using square brackets.

class Specification < ActiveRecord::Base def tolerance

self[:tolerance] or ‘n/a’

end end

class SillyFortuneCookie < ActiveRecord::Base def message=(txt)

self[:message] = txt + ‘ in bed’

end end

Serialized Attributes

One of ActiveRecord’s coolest features (IMO) is the ability to mark a column of type

“text” as being serialized. Whatever object (more accurately, graph of objects) that you assign to that attribute should be represented in the database as YAML, which is Ruby’s native serialization format.

Sebastian Says…

TEXT columns usually have a maximum size of 64K and if your serialized attributes exceeds the size constraints, you’ll run into a lot of errors.

On the other hand, if your serialized attributes are that big, you might want to rethink what you’re doing—At least move them into a separate table and use a larger col- umn type if your server allows it.

CRUD: Creating, Reading, Updating, Deleting

The four standard operations of a database system combine to form a popular acronym: CRUD.

It sounds somewhat negative, because as a synonym for ‘garbage’ or ‘unwanted accumulation’ the word ‘crud’ in English has a rather bad connotation. However, in Rails circles, use of the word CRUD is benign. In fact, as we’ll see in later chapters, designing your app to function primarily as CRUD operations is considered a best practice!

Creating New ActiveRecord Instances

The most straightforward way to create a new instance of an ActiveRecord model is by using a regular Ruby constructor, the class method new. New objects can be instan- tiated as either empty (by omitting parameters) or pre-set with attributes, but not yet saved. Just pass a hash with key names matching the associated table column names.

In both instances, valid attribute keys are determined by the column names of the associated table—hence you can’t have attributes that aren’t part of the table columns.

Newly constructed, unsaved ActiveRecord objects have a @new_recordattribute that can be queried using the method new_record?:

>> c = Client.new

=> #<Client:0x2515584 @new_record=true, @attributes={“name”=>nil,

“code”=>nil}>

>> c.new_record?

=> true

ActiveRecord constructors take an optional block, which can be used to do addi- tional initialization. The block is executed after any passed-in attributes are set on the instance:

>> c = Client.new do |client|

?> client.name = “Nile River Co.”

>> client.code = “NRC”

>> end

=> #<Client:0x24e8764 @new_record=true, @attributes={“name”=>”Nile River Co.”, “code”=>”NRC”}>

ActiveRecord has a handy-dandy create class method that creates a new instance, persists it to the database, and returns it in one operation:

>> c = Client.create(:name => “Nile River, Co.”, :code => “NRC”)

=> #<Client:0x4229490 @new_record_before_save=true, @new_record=false,

@errors=#<ActiveRecord::Errors:0x42287ac @errors={},

@base=#<Client:0x4229490 ...>>, @attributes={“name”=>”Nile River, Co.”, “updated_at”=>Mon Jun 04 22:24:27 UTC 2007, “code”=>”NRC”,

“id”=>1, “created_at”=>Mon Jun 04 22:24:27 UTC 2007}>

Thecreatemethod doesn’t take a block. It probably should, as it feels like a nat- ural place for a block to initialize the object before saving it, but alas it doesn’t.

Reading ActiveRecord Objects

Reading data from the database into ActiveRecord object instances is very easy and convenient. The primary mechanism is the findmethod, which hides SQL SELECT operations from the developer.

find

Finding an existing object by its primary key is very simple, and is probably one of the first things we all learn about Rails when we first pick up the framework. Just invoke

findwith the key of the specific instance you want to retrieve. Remember that if an

instance is not found, a RecordNotFoundexception is raised.

>> first_project = Project.find(1)

>> boom_client = Client.find(99)

ActiveRecord::RecordNotFound: Couldn’t find Client with ID=99 from

/vendor/rails/activerecord/lib/active_record/base.rb:1028:in

`find_one’

from

/vendor/rails/activerecord/lib/active_record/base.rb:1011:in

`find_from_ids’

from

/vendor/rails/activerecord/lib/active_record/base.rb:416:in `find’

from (irb):

Thefindmethod also understands a pair of specially designated Ruby symbols,

:firstand:all:

>> all_clients = Client.find(:all)

=> [#<Client:0x250e004 @attributes={“name”=>”Paper Jam Printers”,

“code”=>”PJP”, “id”=>”1”}>, #<Client:0x250de88

@attributes={“name”=>”Goodness Steaks”, “code”=>”GOOD_STEAKS”,

“id”=>”2”}>]

>> first_client = Client.find(:first)

=> #<Client:0x2508244 @attributes={“name”=>”Paper Jam Printers”,

“code”=>”PJP”, “id”=>”1”}>

Somewhat surprisingly to me, there is no :lastparameter, but you can do a last query pretty easily using the :orderoption:

>> all_clients = Client.find(:first, :order => ‘id desc’)

=> #<Client:0x2508244 @attributes={“name”=>”Paper Jam Printers”,

“code”=>”PJP”, “id”=>”1”}>

By the way, it is entirely common for methods in Ruby to return different types depending on the parameters used, as illustrated in the example. Depending on how

findis invoked, you will get either a single ActiveRecord object or an array of them.

Finally, the find method also understands arrays of keys, and throws a

RecordNotFoundexception if it can’t find all of the keys specified:

>> first_couple_of_clients = Client.find(1, 2)

[#<Client:0x24d667c @attributes={“name”=>”Paper Jam Printers”,

“code”=>”PJP”, “id”=>”1”}>, #<Client:0x24d65b4 @attributes={“name”=>

“Goodness Steaks”, “code”=>”GOOD_STEAKS”, “id”=>”2”}>]

>> first_few_clients = Client.find(1, 2, 3)

ActiveRecord::RecordNotFound: Couldn’t find all Clients with IDs (1,2,3)

from /vendor/rails/activerecord/lib/active_record/base.rb:1042:in

`find_some’

from /vendor/rails/activerecord/lib/active_record/base.rb:1014:in

`find_from_ids’

from /vendor/rails/activerecord/lib/active_record/base.rb:416:in

`find’

from (irb):9

Reading and Writing Attributes

After you have retrieved a model instance from the database, you can access each of its columns in several ways. The easiest (and clearest to read) is simply with dot notation:

>> first_client.name

=> “Paper Jam Printers”

>> first_client.code

=> “PJP”

The private read_attributemethod of ActiveRecord, covered briefly in an ear- lier section, is useful to know about, and comes in handy when you want to override a default attribute accessor. To illustrate, while still in the Rails console, I’ll go ahead andreopen theClientclass on the fly and override the nameaccessor to return the value from the database, but reversed:

>> class Client

>> def name

>> read_attribute(:name).reverse

>> end

>> end

=> nil

>> first_client.name

=> “sretnirP maJ repaP”

Hopefully it’s not too painfully obvious for me to demonstrate why you need

read_attributein that scenario:

>> class Client

>> def name

>> self.name.reverse

>> end

>> end

=> nil

>> first_client.name

SystemStackError: stack level too deep from (irb):21:in `name’

from (irb):21:in `name’

from (irb):24

As can be expected by the existence of a read_attribute method, there is a

write_attributemethod that lets you change attribute values.

project = Project.new

project.write_attribute(:name, “A New Project”)

Just as with attribute getter methods, you can override the setter methods and provide your own behavior:

class Project

# The description for a project cannot be changed to a blank string def description=(new_value)

self[:description] = new_value unless new_value.blank?

end end

The preceding example illustrates a way to do basic validation, since it checks to make sure that a value is not blank before allowing assignment. However, as we’ll see later in the book, there are better ways to do this.

Hash Notation

Yet another way to access attributes is using the [attribute_name]operator, which lets you access the attribute as if it were a regular hash.

>> first_client[‘name’]

=> “Paper Jam Printers”

>> first_client[:name]

=> “Paper Jam Printers”

String Versus Symbol

Many Rails methods accept symbol and string parameters interchangeably, and that is potentially very confusing.

Which is more correct?

The general rule is to use symbols when the string is a name for something, and a string when it’s a value. You should probably be using symbols when it comes to keys of options hashes and the like.

Common sense dictates picking one convention and stick- ing to it in your application, but most Rails people will use symbols everywhere possible.

TheattributesMethod

There is also an attributesmethod that returns a hash with each attribute and its corresponding value as returned by read_attribute. If you use your own custom attribute reader and writer methods, it’s important to remember that attributeswill notuse custom attribute readers when accessing its values, but attributes=(which lets you do mass assignment) doesinvoke custom attribute writers.

>> first_client.attributes

=> {“name”=>”Paper Jam Printers”, “code”=>”PJP”, “id”=>1}

Being able to grab a hash of all attributes at once is useful when you want to iter- ate over all of them or pass them in bulk to another function. Note that the hash returned from attributes is not a reference to an internal structure of the ActiveRecord object—it is copied, which means that changing its values will have no effect on the object it came from:

>> atts = first_client.attributes

=> {“name”=>”Paper Jam Printers”, “code”=>”PJP”, “id”=>1}

>> atts[“name”] = “Def Jam Printers”

=> “Def Jam Printers”

>> first_client.attributes

=> {“name”=>”Paper Jam Printers”, “code”=>”PJP”, “id”=>1}

To make changes to an ActiveRecord object’s attributes in bulk, it is possible to pass a hash to the attributeswriter.

Accessing and Manipulating Attributes Before They Are Typecast

The ActiveRecord connection adapters fetch results as strings and Rails takes care of converting them to other datatypes if necessary, based on the type of the database col- umn. For instance, integer types are cast to instances of Ruby’s Fixnumclass, and so on.

Even if you’re working with a new instance of an ActiveRecord object, and have passed in constructor values as strings, they will be typecast to their proper type when you try to access those values as attributes.

Sometimes you want to be able to read (or manipulate) the raw attribute data without having the column-determined typecast run its course first, and that can be done by using the <attribute>_before_type_cast accessors that are automati- cally created in your model.

For example, consider the need to deal with currency strings typed in by your end users. Unless you are encapsulating currency values in a currency class (highly recom- mended, by the way) you need to deal with those pesky dollar signs and commas.

Assuming that our Timesheetmodel had a rate attribute defined as a :decimaltype, the following code would strip out the extraneous characters before typecasting for the save operation:

class Timesheet < ActiveRecord::Base before_save :fix_rate

def fix_rate

rate_before_type_cast.tr!(‘$,’,’’) end

end

Reloading

The reload method does a query to the database and resets the attributes of an ActiveRecord object. The optional options argument is passed to find when reloading so you may do, for example, record.reload(:lock => true)to reload the same record with an exclusive row lock. (See the section “Database Locking” later in this chapter.)

Dynamic Attribute-Based Finders

Since one of the most common operations in many applications is to simply query based on one or two columns, Rails has an easy and effective way to do these queries without having to resort to the conditions parameter of find. They work thanks to the magic of Ruby’s method_missing callback, which is executed whenever you invoke a method that hasn’t been defined yet.

Một phần của tài liệu The rails way (Trang 217 - 370)

Tải bản đầy đủ (PDF)

(910 trang)