Tài liệu hạn chế xem trước, để xem đầy đủ mời bạn chọn Tải xuống
1
/ 44 trang
THÔNG TIN TÀI LIỆU
Thông tin cơ bản
Định dạng
Số trang
44
Dung lượng
381,45 KB
Nội dung
Soft programmatic extension of models 397 these particular models, and they’ve been chosen to represent a broad range of possibilities. But the point is that the door is open. As you become increasingly familiar and comfortable with Ruby, writing this kind of code will come more and more naturally. We’ll work model by model, adding soft programmatic enhancement to the Work, Customer, and Composer models. To give you an overview, here’s a list of all the questions that we’ll ask and answer by writing methods in this section: ■ The Work model ● Which publishers have published editions of this work? ● What country is this work from? ● What key is this work in? ■ The Customer model ● Which customers have ordered this work? ● What open orders does this customer have? ● What editions does this customer have on order? ● What editions has this customer ever ordered? ● What works does this customer have on order? ● What works has this customer ever ordered? ■ The Composer model ● What editions of this composer’s works exist? ● What publishers have editions of this composer’s works? As you’ll see, all of these methods involve giving the models a boost—a way to unearth and collect existing information that isn’t available already in the form we want it through an existing method. NOTE PLAIN ARRAYS VS. MAGIC COLLECTIONS You should keep one important point in mind as you look at, and eventually write, soft enhancements to your models. When you gather together, say, an array of Edition objects by traversing a collection of Work objects and accumulating their editions, you end up with a plain Ruby array. You don’t end up with a magic ActiveRecord collection. You’ll recall that in discussing arrays in chapter 11, we looked at ActiveRecord collections as an example of some- thing array-like that is also endowed with methods and behaviors that go beyond those of the normal array. Those extra powers aren’t added to arrays that you create, even if they contain ActiveRecord objects. 398 CHAPTER 15 Programmatically enhancing ActiveRecord models 15.2.1 Honing the Work model through soft enhancements The soft programmatic extensions to the Work model involve mining the model and its associated entities for a next level of information. This information, in turn, might be used for in-house reports, richer on-screen information displays, or sales profiling. All these Work enhancements belong in the work model file, work.rb . Which publishers have published editions of this work? This method uses the same basic approach as the editions method that served as an introductory example in section 15.1. In fact, it builds on that method: It calls editions and uses map on the resulting array to extract the publisher of each edi- tion. It then performs a uniq operation, resulting in a nonduplicative list of all publishers who have published this work: def publishers editions.map {|e| e.publisher}.uniq end This technique skims the publishers from the editions, producing a list of the latter. What country is this work from? A case could be made for assigning the work either to the native country of the composer or to the country of first publication. Because we’re dealing with a sheet-music store and not a library, we don’t necessarily know what the first publi- cation was. That means if we want to assign a country to a work, it’s best to echo the composer’s country. This is an easy soft enhancement to the Work model: def country composer.country end The enhancement qualifies as soft because it’s passive: It reaches out one level, from the work to its composer, and gathers information, which it returns unchanged. Which customers have ordered this work? A method like this could conceivably be of interest in calculating sales figures and trends. Once again, we use the editions method as a point of entry for gathering further information. In this case, we map all the existing orders for all editions of this work—and from that mapping, we harvest the customers of the orders: def ordered_by editions.orders.map {|o| o.customer }.uniq end Soft programmatic extension of models 399 We then make the resulting array unique, in case any customer has purchased two different editions of the work or purchased one edition twice. For some pur- poses, you may want to keep such duplicates—for example, if you’re trying to determine a work’s popularity (in which case someone who bought every edition of it might legitimately be counted multiple times). But assuming that you’re interested simply in a list of customers who have bought this work, there’s no point saving the duplicates. What key is this work in? You might not have been expecting this to be one of the enhancements, because the work’s key is already stored directly in the database. But remember: key is a reserved word in SQL, so we named the field containing the key kee. The enhancement we need is one that will let us use key as a method name to get the key of a work. It’s simple: def key kee end Now, when we ask a work for its key, it will tell us its kee, which is what we really want to know. 15.2.2 Modeling the customer’s business In the case of the customer, we want to know a number of things. Some of these methods are layered on, or embedded into, others. Some will be of direct use at the controller/view stage. What open orders does this customer have? We’ll write open_orders to return an ActiveRecord collection: def open_orders orders.find(:all, :conditions => "status = 'open'") end Although we may treat the resulting collection in most contexts as a normal array, having it be an ActiveRecord collection means that, if the occasion arises, we’ll be able to query it using the hybrid Ruby/ SQL semantics that such collec- tion objects allow. What editions does this customer have on order? Here, we use standard Ruby array methods to grab all the editions this customer has ordered: 400 CHAPTER 15 Programmatically enhancing ActiveRecord models def editions_on_order open_orders.map {|order| order.edition }.uniq end First, we generate an array of editions by iterating through the customer’s orders and skimming off each order’s edition object (courtesy of map , which will return a new array). Then, we run the results through uniq , producing a list of editions without regard to how many copies of each have been ordered. What editions has this customer ever ordered? This method is a superset of editions_on_order , returning a list of all the editions this customer has ever ordered. This information will be useful in calculating a customer’s favorites (favorite composers and/or instruments): def edition_history orders.map {|order| order.edition }.uniq end We’ll now do for works what we’ve already done for editions: provide a way to grab the works that are on order (regardless of what edition each one is in) and a way to generate a list of every work the customer has ever ordered. What works does this customer have on order? Here, we start with editions_on_order and then dig into the contents of the edi- tion (its list of works): def works_on_order editions_on_order.map {|edition| edition.works }.flatten.uniq end Mapping that operation across all the editions on order returns one array of works for each edition—overall, an array of arrays. We want a flat array of works, so we flatten it and then run it through uniq to get rid of duplicate works. What works has this customer ever ordered? This method is like the previous one, but it gathers works from all editions, not just those on order: def work_history edition_history.map {|edition| edition.works }.flatten.uniq end All the methods you’ve now added to the Customer class involve looking through lists of entity model instances (ActiveRecord objects) and returning transformed lists. The remaining Customer methods fall into the category of hard model Soft programmatic extension of models 401 enhancement; they involve calculating a new value from existing data. We’ll leave those for the next section, and turn briefly to the composer. 15.2.3 Fleshing out the Composer Composers are a fairly inactive element in the universe of our domain. They don’t change much, and most of them have stopped composing, so there’s not as much need to provide them with a data-manipulation toolset as there is with some of the other models. We’ll define only two composer instance methods; they go in the Composer class, in composer.rb . What editions of this composer’s works exist? This is the method that served as the preliminary example of a soft model enhancement: def editions works.map {|work| work.editions }.flatten.uniq end This method is used for the purpose of generating a list of editions to be embed- ded in a clickable list—something we’ll include among the new and enhanced music store views in the next chapter. What publishers have editions of this composer’s works? This method may possibly be of use only for internal accounting purposes—but we’ll throw it in for good measure and as a lesson in how easy it is to expand your application’s repertoire of methods: def publishers editions.map{|edition| edition.publisher }.uniq end That brings us to the end of our list of questions and the corresponding soft enhancements of the R4RMusic models. Our next big topic is hard programmatic enhancements: methods that go as far as you want them to in manipulating data and creating new objects and data structures. First, by way of final reflection on soft enhancements, a few words are in order about the relationship between Ruby code and SQL—or, more accurately, the pro- cess of choosing between Ruby and SQL—in the writing of soft enhancements. 15.2.4 Ruby vs. SQL in the development of soft enhancements When you write code whose main purpose is to pull records out of a relational data- base, the most efficient, fastest code you can write is SQL code. As you probably 402 CHAPTER 15 Programmatically enhancing ActiveRecord models know, much of what ActiveRecord does for you under the hood is to translate your Ruby code into SQL statements and then query your application’s databases with those statements. In the interest of increasing execution speed, ActiveRecord lets you feed it pure SQL almost whenever you want. You lose the nice Ruby-wrapped look-and- feel, but you gain efficiency. As a study in Ruby/ SQL contrast, take the Composer#editions method from section 15.2.3: def editions works.map {|work| work.editions }.flatten.uniq end This method starts by unconditionally gathering all the works by this composer, which it does by calling works . At this point, ActiveRecord is finished; the one and only database query required here returns all the works for this composer. What remains is pure Ruby: harvesting the editions of the works (courtesy of map ) and massaging the resulting array-of-arrays of editions with flatten and uniq . Each of these operations creates a new array—potentially a large one, if we’re dealing with a well-stocked music store. Here’s an alternative editions method, written using SQL instead of Ruby to narrow the selection of Edition objects: def editions ddddEdition.find_by_sql("SELECT edition_id from editions_works ddddLEFT JOIN works ON editions_works.work_id = works.id ddddLEFT JOIN composers ON works.composer_id = composers.id ddddWHERE (composers.id = #{id})") end This method asks the database engine to do the work. By the time the single call to find_by_sql is finished, we have all the editions we need; no further Ruby com- mands are required. Database engines such as MySQL tend to be efficient (at least, ideally). Asking for the right records in the first place, rather than asking for more records than you need and then pruning them in Ruby, is faster and more efficient. But it also means you have to write everything in SQL—which is not necessarily a hardship from the point of view of programming but does destroy your pro- gram’s consistent look and feel. Nor is this issue entirely cosmetic. The consistent “Rubyness” of a Rails application makes for a consistent development experience: It’s easier to think in Ruby the whole time than it is to switch back and forth. (You Soft programmatic extension of models 403 have to do some switching anyway, if you’re writing the database; but the ideal is to keep that process as separate as possible from the higher-level coding.) Because it involves hard-coding table and field names into your Ruby methods, doing soft enhancements in SQL has the potential to make the application code harder to maintain later on. True, you can’t write a Rails application without knowing the table and field names; but having them physically present in your model code takes the coupling of database and code a step further. But it will make your application faster, as well as giving you “magic” ActiveRecord collec- tions rather than standard Ruby arrays as containers for your objects. What’s the right choice? Not surprisingly, it all depends. Luckily, you don’t have to make an all-or-nothing, winner-take-all choice between Ruby and SQL as model enhancement languages. Rails is designed in full knowledge of the pros and cons of SQL versus pure Ruby. The existence of the find_by_sql method attests to this fact; so does the use of SQL fragments to specify record order (as in :order => "created_at, ASC" , an SQL hint used in the customer’s has_many :orders association). The reality of relational database programming is that you should know some SQL if you’re going to do it, even at one level of remove—and Rails facilitates your using SQL when you want to. The philosophy of this book is that it’s good to use Ruby to enhance the func- tionality of models until you hit a performance wall and have to use raw SQL. The relationship between Ruby and SQL, in this context, isn’t unlike the relationship between Ruby and C in the general Ruby-programming context: Ruby program- mers write in Ruby, knowing that it isn’t a terribly fast language; when they hit seri- ous performance bottlenecks, they write parts of their programs as C extensions, so that those parts will speed up and the whole program will run faster. SQL can play a similar role for you in your Rails application development. Think of Rails applications as Ruby programs, first and foremost. But by all means take advantage of the options that ActiveRecord gives you, by way of using SQL, when you spot something you’ve written in Ruby that seems to be seriously slow- ing your program. This meditation on SQL and Ruby truly brings us to the end of our soft pro- grammatic enhancement discussion and to our next major topic: hard program- matic enhancements of ActiveRecord models. 404 CHAPTER 15 Programmatically enhancing ActiveRecord models 15.3 Hard programmatic enhancement of model functionality In this section, we’re going to pull out the Ruby stops and show how you can add new functionality to your models that may not have any direct relation to the models’ basic properties and capabilities. Basically, you can define any method for your models to respond to. The idea isn’t to create chaos, but to come up with things you might want to know. The examples here are clustered by type of example rather than in a question- and-answer format. This reflects the fact that hard enhancements tend to have a purpose other than straightforward querying of an object for information; they entail the creation of a new object or data structure rather than a culling of exist- ing objects. In the sections that follow, we’ll develop hard programmatic enhancements of several of the R4RMusic models. The enhancements fall into three categories: ■ Prettification of string properties ■ Calculating a work’s period ■ Providing the customer with more functionality Your Ruby skills will get a workout here, and you’ll learn a few new techniques along the way. 15.3.1 Prettification of string properties A common use for hard model enhancements is the prettification of string proper- ties—the generation of a new string in which existing string information is embedded and which looks better, for presentation, than the raw string data avail- able through the object would look. We’ve already seen one example of prettification of strings: the Com- poser#whole_name method defined for the purpose of easily displaying all the components of a composer’s name together. This kind of thing can come in handy frequently and can involve greater complexity and planning than just stringing strings together. We’ll look at some examples here. Formatting the names of the work’s instruments The Work model is a good candidate for some pretty-formatting operations. It has a title, an opus number, and a list of instruments, all of which are stored in raw form and are in need of massaging on the way to public viewing. Hard programmatic enhancement 405 of model functionality We’ll begin with the instruments, because the resulting list will be of use in the title. Let’s start with the nice_instruments method, an instance method of the Work class in work.rb , like this: def nice_instruments instrs = instruments.map {|inst| inst.name } This map operation skims the name values from the list of instrument objects and stores them in a new array, which we save to the variable instrs . The next step (almost) is to format these names into a nice string. There’s one intermediate tweak, though. It has to do with the order of instruments: cello and piano, or piano and cello? We’ll handle this in the following way. First, we create an array of instrument names in what we consider the canonical (or at least likely to be correct almost every time) order. Incidentally, you’ll encounter a new technique in this line of code: the %wf{…} construct, which generates an array whose elements are the indi- vidual words inside the curly braces. ordered = %w{ flute oboe violin viola cello piano orchestra } Next, we sort instrs according to where in this array (at what numbered index) each instrument occurs. Because it’s possible that we’ll encounter an instrument that isn’t on this list, if no index is found we return 0, which in a sorting context means equal to: instrs = instrs.sort_by {|i| ordered.index(i) || 0 } We can also put the list of ordered instruments in a constant at the top of the model file and then refer to that constant in the method. That would probably make the list easier to maintain. It still has the disadvantage of having to be updated manually, but in a production environment you could ensure that every time a new instrument was introduced into the universe, a decision would have to be made about where it fitted into the list. (You could also start with a much big- ger list, of course.) We now have a list of instrument names sorted according to conventional instrument-listing semantics. What we now do with those names, for purposes of inserting them into the nice title of the work, depends on how many there are: ■ If there are none, we want nil (not an empty string, for reasons that will become apparent when we put together the whole title). ■ If there’s just one, we want it by itself (Partita for Violin). 406 CHAPTER 15 Programmatically enhancing ActiveRecord models ■ If there are two, we want them joined by the word and (Sonata for Violin and Piano). ■ If there are three or more, we want to join them with commas—except the last two, which are additionally joined by and (Trio for Violin, Cello, and Piano). It will be a matter of testing the size of instrs and proceeding accordingly. We can do this with a case statement, with separate branches for each of the four possibilities: ddcase instrs.size ddwhen 0 ddddnil ddwhen 1 ddddinstrs[0] ddwhen 2 ddddinstrs.join(" and ") ddelse ddddinstrs[0 2].join(", ") + ", and " + instrs[-1] ddend end (You can see the code grow in length, as well as complexity, as the number of instruments in the work increases!) The last case—more than two names—is worth examining up close. It uses the trick of grabbing all elements of the array except the last: instrs[0 2] Negative array indices are counted from the right, so instrs[-2] is the second-to- last item in the array. All of the items thus selected then get joined with commas. To the resulting substring, we add the string “, and ” followed by the last item in the array ( instrs[-1] ). Because we’ve built it in fragments, here’s the full nice_instruments method in one place: def nice_instruments ddinstrs = instruments.map {|inst| inst.name } ddordered = %w{ flute oboe violin viola cello piano orchestra } ddinstrs = instrs.sort_by {|i| ordered.index(i) || 0 } ddcase instrs.size ddwhen 0 ddddnil ddwhen 1 ddddinstrs[0] ddwhen 2 ddddinstrs.join(" and ") ddelse [...]... reasonably well-formatted, descriptive string for later insertion into the nice title Formatting a work’s opus number Let’s prettify the opus number next As you’ll recall, the opus field in the database holds a string Due to the vagaries of indexing systems, several formats are possible for entries in this field: ■ Plain opus number (“1 29 ) ■ Opus number plus number designation (“1 29 no.4”) ■ Special... method, two_dec, formats a floating-point number as a string to exactly two decimal places Its purpose is to make sure prices are displayed in correct dollars-and-cents format To achieve this, we use the built-in Ruby utility method sprintf; like the C method of the same name, sprintf interpolates values into a string, using format specifiers in the string to format the values correctly The format specifier... the sequence “, for ” + nice_instruments The main thing is that if no key is indicated, we don’t want the word in, and likewise for the connecting strings for opus and instruments We therefore have to put the nice title string together conditionally We do this as follows: def nice_title ddt,k,o,i = title, key, nice_opus, nice_instruments dd"#{t} #{"in #{k}" if k}#{", #{o}" if o}#{", for #{i}" if i}"... context; nil evaluates to false It’s possible to test a string for emptiness with empty?, but using nil allows for a quick Boolean check.) Hard programmatic enhancement of model functionality 4 09 All told, you can get quite a bit of prettification mileage out of a decent knowledge of how to manipulate, test, and combine strings in Ruby A nice title for the Edition model When it comes to titles of editions,... combination of time and place For example, we might want British music of the nineteenth century (at least, most of it) to be described as Victorian, whereas that term wouldn’t make sense for music from Italy or France We’re looking for a Ruby data structure that lets us make connections among time spans, countries, and descriptive period names There are a couple of tools we can reach for One possibility is... ddrank(work_history.map {|work| work.instruments }.flatten) end Here’s a walk-through of how these methods work We’ll use instruments for this example and refer to them by name for simplicity Let’s say someone orders: ■ A work for cello and piano ■ A work for cello and orchestra ■ A work for orchestra That means our pre-flattened instrument history is [["cello", "piano"], ["cello", "orchestra"], ["orchestra"]]... by value We can do something similar for composers Summary 421 Determining sales rankings for composers We can use Work.sales_rankings as the basis for calculating sales rankings for composers This method goes in app/models/composer.rb: def Composer.sales_rankings r = Hash.new(0) ddWork.sales_rankings.map do |work,sales| r[work.composer.id] += sales end r end For each work in the sales rankings hash,... the nice components in place, with a couple of connector words The format is represented by this example: Sonata in F Major, op 99 , for cello and piano 408 CHAPTER 15 Programmatically enhancing ActiveRecord models More is going on here than retrieving the parts We’re also connecting them with a mixture of commas, spaces, and the word for The elements of the title are as follows: ■ Title ■ If there’s... reminder that we don’t yet have a controller file for instruments That’s easy to fix (and worth a brief detour) First, give the usual command for controller creation: $ ruby /script/generate controller instrument show This creates the necessary files for the instrument controller; and in the principal file, instrument_controller.rb, is an empty definition for the show method That method needs to be fleshed... supplies a pretty full toolkit for those operations) Still, some of the class methods you write for your model classes will be more active than others Determining all editions for a list of works This is a specialized method, but it will come in handy at least once when we get to view and controller enhancement in chapter 16 It’s a class method on the Edition class, and therefore it belongs in edition.rb . the model and its associated entities for a next level of information. This information, in turn, might be used for in-house reports, richer on-screen information displays, or sales profiling cosmetic. The consistent “Rubyness” of a Rails application makes for a consistent development experience: It’s easier to think in Ruby the whole time than it is to switch back and forth. (You Soft programmatic. The relationship between Ruby and SQL, in this context, isn’t unlike the relationship between Ruby and C in the general Ruby- programming context: Ruby program- mers write in Ruby, knowing that it