How to optimize ActiveRecord in Ruby on Rails _ruby topics

Source: Internet
Author: User
Tags inheritance object model ruby on rails


Ruby on Rails programming often spoils you. This evolving framework frees you from the tedium of other frameworks. You can express your intent with a few lines of code that you are accustomed to. You can also use ActiveRecord.



For an old Java like me? Programmers, ActiveRecord is a little rusty. Through the Java framework, I usually build a mapping between independent models and schemas. A framework like this is the mapping framework. With ActiveRecord, I only define the database schema: either SQL or a Ruby class called migration (migration). The frameworks that set object model design on top of the database structure are called wrapper frameworks. Unlike most packaging frameworks, Rails can discover the characteristics of an object model by querying database tables. Unlike building complex queries, I use models to traverse relationships in Ruby, not SQL. As a result, I have both the simplicity of the packaging framework and most of the functionality of the mapping framework. ActiveRecord is easy to use and extend. Sometimes, even some are too simple.



Like any database framework, ActiveRecord makes it very easy for me to make a lot of trouble. I can get too many columns and easily omit important structured database features, such as indexes or empty constraints. I'm not saying that ActiveRecord is a bad frame. Only if you need to expand, you need to know how to strengthen your application. In this article, I'll take you through some of the important optimizations that you might need when using the unique persistence framework of Rails.
Basic Management



Generating model-supported models is exceptionally easy, requiring little code, that is, script/generate model Model_name. As you know, the command can generate models, migrations, unit tests, and even a default fixture. It's tempting to fill in some data columns in the migration and enter some test data, write several tests, and add a few validations. But please reconsider. You should consider the overall database design, paying special attention to these things:


    • Rails will not allow you to get rid of basic database performance issues. The database needs information that is often indexed for good performance.
    • Rails will not let you get rid of data integrity issues. Although most Rails developers don't like to keep restrictions in the database, you should consider things like empty columns.
    • Rails provides convenient default properties for many elements. Sometimes, default properties such as the length of a text field can be too large for most practical applications.
    • Rails does not force you to create an effective database design.


Before you continue your Trek and study ActiveRecord, you should first make sure you have a good enough foundation. Make sure that the index structure is available to you. If the given table is large and if you are searching on columns instead of IDs, if the index can help you (see Database Manager documentation in different ways for different databases to use indexes differently), you need to create an index. You do not need to create an index with SQL--you can simply use migration creation. You can easily create an index using the create_table migration, or you can create an additional migration to create an index. The following is a migration example that you can use to create an index for changingthepresent.org (see Resources):
Listing 1. To create an index in the migration


Class Addindexestousers < activerecord::migration
 def self.up
  add_index:members: Login
  add_index: Members: Emails
  add_index:members,: first_name
  add_index:members,: Last_Name end

 def Self.down
  remove_index:members,: Login
  remove_index:members,: Email
  remove_index:members,: first_name
  Remove_index:members,: last_name
 End



ActiveRecord will be responsible for the index on the ID, and I have explicitly added the indexes that can be used in various searches because the table is large, infrequently updated, and often searched. Usually, we wait until we have some certainty about the problem in the given query before we take the appropriate action. This strategy allows us to guess the database engine no two times. But from the point of view of the user, we know that the table will soon have millions of users, and if there are no indexes on the frequently searched columns, the table is inefficient.



The other two common problems are also related to migrations. If neither the string nor the column should be empty, make sure that the migration is correctly written. Most DBAs (database administrators) think that Rails provides the wrong default property for empty columns: The default can be null. If you want to create a column that cannot be empty, you must explicitly add the parameter: null => false. If you have a string column, be sure to write the application's limit value. By default, Rails migration encodes the string column by varchar (255). Usually, this value is too large. The database structure of the application should be kept as faithfully as possible. Instead of providing a login without any restrictions, if the application restricts login to only 10 characters, you should write the database accordingly, as shown in Listing 2:
Listing 2. To write a migration with a limit value and a non-empty column


T.column:login,: String,: Limit =>: null => false


In addition, you should consider the default values and any other information that can be safely provided. With a little bit of prep work, you can save a lot of time to track data integrity issues later. While considering the basics of your database, you should also note which pages are static and easy to cache. In the two options of optimizing the query and caching pages, if you can "digest" the Complexity, caching the pages will yield greater rewards. Sometimes, pages or fragments are purely static, such as a list of States or a group of frequently asked questions. In this case, the cache is more than a chip. At other times, you may decide to sacrifice database performance to reduce complexity. For Changingthepresent, we both tried, depending on the circumstances of the problem and the environment. If you also decide to sacrifice query performance, please read on.
N+1 problem



By default, ActiveRecord relationships are very lazy. This means that the framework waits for access until you actually access the relationship. For example, each member will have an address. You can open a console and enter the following command: member = Member.find 1. You can see the following additions to the log, as shown in Listing 3:
Listing 3. Login from Member.find (1)


^[[4;35;1mmember Columns (0.006198) ^[[0m  ^[[0mshow FIELDS from members^[[0m ^[[4;36;1mmember
Load (0.002835) ^ [[0m  ^[[0;1mselect * from the Members WHERE
 (members. ' id ' = 1] ^[[0m


Member has a relationship to this address and is has_one:address by macros: As =>: addressable,:d ependent =>:d estroy definition. Note When ActiveRecord loads member, you do not see the Address field. But if you type member.address in the console, you can see the contents of Listing 4 in Development.log:
Listing 4. Access Relationships Force Database access


 ^[[36;2m./vendor/plugins/paginating_find/lib/paginating_find.rb:98:in ' Find ' ^[[0m
^[[4;35;1maddress Load ( 0.252084) ^[[0m  ^[[0mselect * from addresses WHERE
 (addresses.addressable_id = 1 and Addresses.addressable_type = ' Member '] LIMIT 1^[[0m
 ^[[35;2m./vendor/plugins/paginating_find/lib/paginating_find.rb:98:in ' Find ' ^[[0m


So ActiveRecord does not execute the query for the address relationship until you actually access member.address. Typically, this lazy design works well because the persistence framework does not need to move so much data to load members. But if you want to access many members and the addresses of all members, as shown in Listing 5:
Listing 5. Retrieving multiple members by address


Member.find ([1,2,3]). Each {|member| puts member.address.city}


Because you should see queries for each address, the results are not as satisfactory in terms of performance. Listing 6 shows all of the problems:
Listing 6. Queries for n+1 problems


^[[4;36;1mMember Load (0.004063)^[[0m  ^[[0;1mSELECT * FROM members WHERE
 (members.`id` IN (1,2,3)) ^[[0m
 ^[[36;2m./vendor/plugins/paginating_find/lib/paginating_find.rb:98:in `find'^[[0m
^[[4;35;1mAddress Load (0.000989)^[[0m  ^[[0mSELECT * FROM addresses WHERE
 (addresses.addressable_id = 1 AND addresses.addressable_type = 'Member') LIMIT 1^[[0m
 ^[[35;2m./vendor/plugins/paginating_find/lib/paginating_find.rb:98:in `find'^[[0m
^[[4;36;1mAddress Columns (0.073840)^[[0m  ^[[0;1mSHOW FIELDS FROM addresses^[[0m
^[[4;35;1mAddress Load (0.002012)^[[0m  ^[[0mSELECT * FROM addresses WHERE
 (addresses.addressable_id = 2 AND addresses.addressable_type = 'Member') LIMIT 1^[[0m
 ^[[35;2m./vendor/plugins/paginating_find/lib/paginating_find.rb:98:in `find'^[[0m
^[[4;36;1mAddress Load (0.000792)^[[0m  ^[[0;1mSELECT * FROM addresses WHERE
 (addresses.addressable_id = 3 AND addresses.addressable_type = 'Member') LIMIT 1^[[0m
 ^[[36;2m./vendor/plugins/paginating_find/lib/paginating_find.rb:98:in `find'^[[0m


The result was as bad as I had foreseen. All members share one query, and each address uses one query. We searched for three members, so we used four queries altogether. If it is N members, there will be a n+1 query. This is a terrible n+1 problem. Most persistence frameworks use hot associations (eager association) to solve the problem. Rails is no exception. If you need an access relationship, you can optionally include it in the initial query. ActiveRecord use the Include option for this purpose. If you change the query to Member.find ([1,2,3],: include =>: Address). Each {|member| puts member.address.city}, the result is slightly better:
Listing 7. Resolving n+ 1 Questions


^[[4;35;1mmember Load including associations (0.004458) ^[[0m  ^[
  [0mSELECT members. ' IDs ' as t0_r0, members. ' Type ' As T0_R1, members of the '
  About_me ' as T0_R2, members. ' About_philanthropy '

  ...

  addresses. ' id ' as T1_R0, addresses. ' Address1 ' as T1_R1,
  addresses. ' Address2 ' as T1_R2, addresses. ' City ' as T1_r3,...

  Addresses. ' addressable_id ' as t1_r8 from, left,
  OUTER JOIN addresses on
  addresses.addressable_id = Members.id and Addresses.addressable_type =
  ' member ' WHERE (members. ' IDs ' in (1,2,3)) ^[
  [0m
 ^[[35;2m./ VENDOR/PLUGINS/PAGINATING_FIND/LIB/PAGINATING_FIND.RB:
 98:in ' Find ' ^[[0m


The query can also be faster. A query retrieves all the members and addresses. This is how the thermal association works.



With ActiveRecord, you can also nest: include options, but the nesting depth is only one level. This is the case, for example, of a multiple contacts member and a contact with an address. If you want to show all cities for a member's contacts, you can use the code shown in Listing 8:
Listing 8: Getting a city for a member's contact


member = Member.find (1)
Member.contacts.each {|contact| puts contact.address.city}


The code should work, but it must be queried for this member, for each contact, and for the address of each contact. By using: Include =>: Contacts includes: Contacts, you can slightly improve performance. You can also make further improvements by including both, as shown in Listing 9:
Listing 9: Getting a city for a member's contact


member = Member.find (1)
Member.contacts.each {|contact| puts contact.address.city}


Better improvements can be achieved by using nested inclusion options:


member = Member.find (1,: Include => {: Contacts =>: Address})
Member.contacts.each {|contact| puts Contact.address.city}


This nesting contains contacts and address relationships that allow Rails heat to be included. When you want to use relationships in a given query, you can adopt a thermal loading technique. This technique is one of the most frequently used performance optimization techniques in changingthepresent.org, but it has some limitations. When you have to connect more than two tables, it is best to use SQL. If you need to report, it is best to simply take a database connection, spanning ActiveRecord and Activerecord::base.execute ("SELECT * from ..."). Generally speaking, hot correlation is enough to solve the problem. Now, I'm going to change the subject and explore another troublesome issue that Rails developers care about: inheritance.
Inheritance and Rails



When most rails developers are exposed to rails for the first time, they are instantly fascinated. It's too easy. You simply create a type class on the database table and then inherit the subclass from the parent class. Rails will be responsible for the rest of the work. For example, there is a Customer table named, which can inherit from the name of the person class. A customer can have all the columns of person, plus credit and order history. Listing 10 shows the simplicity of the solution. The primary table has all the columns of the parent class and the child class.
listing 10. Implementing Inheritance


Create_table "People" Do |t|
 T.column ' type ',: String
 t.column ' first_name ',: string
 t.column ' last_name ',: string
 t.column ' Loyalty_ Number ",: String
end

class Person < ActiveRecord::Base
End

class Customer < person
 Has_ Many:orders End



In many ways, this kind of solution can work well. The code is simple and not repeatable. These queries are simple and performance-free because you do not need to make any connections to access multiple subclasses, ActiveRecord can use the Type column to determine which records can be returned.



In some respects, ActiveRecord inheritance is very limited. If an existing inheritance level is very wide, inheritance is invalidated. For example, in Changingthepresent, there are many types of content, each with its own name, a short or long description, some common presentation properties, and several custom attributes. We really want cause, nonprofit, Gift, member, drive, registry, and other types of objects to inherit from a common base class so that we can handle all types of content in the same way. But we can't, because the Rails model will have the substance of all our object models in a single table, which is not a viable solution.
Explore other options



We have tested three solutions for this problem. First, we place each class in the table of the class itself and use the view to build the common table for the content. We quickly abandoned this solution because Rails did not work well with database views.



Our second solution is to use simple polymorphism. With this strategy, each subclass will have its own table. We push the universal columns into each table. For example, let's say I need a subclass named Content that contains only the name attribute and the Gift, Cause, and nonprofit subclasses. Gift, nonprofit, and cause can all have the name attribute. Because Ruby is dynamically typed, these subclasses do not need to inherit from a universal base class. They only need to respond to the same set of methods. Changingthepresent uses polymorphism in several places to provide common behavior, especially when dealing with images.



The third approach is to provide a common feature, but it takes an association rather than an inheritance. ActiveRecord has an attribute called a polymorphic association, which is ideal for attaching common behavior to a class without inheriting at all. In the previous address, you have seen examples of polymorphic associations. I can use the same technology (not inheritance) to attach common attributes for content management. Consider the class named Contentbase. In general, in order to associate the class to another class, you can use the Has_one relationship and a simple foreign key. But you might want to let contentbase work with multiple classes. At this point, you need a foreign key, and you need a column that defines the type of the target class. And that's exactly what ActiveRecord polymorphic associations are good at. Please refer to listing 11.
Listing 11. Two aspects of site content relationships


Class cause < ActiveRecord::Base
 Has_one:content_base: As =>:d isplayable,:d ependent =>:d Estroy
 ...
End

class nonprofit < ActiveRecord::Base
 has_one:content_base: As =>:d isplayable,:d ependent = >:d Estroy ...
End


class Contentbase < activerecord::base
 belongs_to:d isplayable,:p olymorphic => true



Usually, there is only one class of belongs_to relationship, but the relationship in Contentbase is polymorphic. A foreign key has not only an identifier to identify the record, but also a type that identifies the table. Using this technique, I gained a lot of benefits from inheritance. Common features are included in a single class. But it also brings a few side effects. I don't have to put all the columns in cause and nonprofit in a single table.



Some database administrators are not very optimistic about polymorphic associations because they do not use foreign keys in real sense, but for changingthepresent we are free to use polymorphic associations. In fact, the data model is not as good as it is theoretically. You cannot use database attributes such as referential integrity, nor do you rely on tools to discover these relationships based on the name of the column. The benefits of a concise object model are more important to us than the problems of this approach.


Create_table "Content_bases",: Force => true do |t|
 T.column "Short_description",     : String

 ...

 T.column "Displayable_type",: String
 t.column "displayable_id",  : Integer
end


Conclusion



ActiveRecord is a durable framework with perfect function. It allows you to build scalable, reliable systems, but like other database frameworks, you must pay extra attention to the SQL generated by the framework. When you encounter problems occasionally, you must adjust your own ways and policies. Preserving indexes, using hot loading with include, and using polymorphic associations in some places to replace inheritance are three ways you can improve your code base. Next month, I'll take you through another example of how to write real-world Rails.


Related Article

Contact Us

The content source of this page is from Internet, which doesn't represent Alibaba Cloud's opinion; products and services mentioned on that page don't have any relationship with Alibaba Cloud. If the content of the page makes you feel confusing, please write us an email, we will handle the problem within 5 days after receiving your email.

If you find any instances of plagiarism from the community, please send an email to: info-contact@alibabacloud.com and provide relevant evidence. A staff member will contact you within 5 working days.

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.