6

I have entries table with a content field which might contain a lot of text. In most cases I don't need to access that field, so it seems to be a big waste of resources to every time load a huge amount of unused data from the database (select * from entries where id = 1).

How could I specify the default_scope, that all the fields apart from content would be loaded from database?

krn
  • 6,715
  • 14
  • 59
  • 82

4 Answers4

13

Assuming Rails 3 and a schema that looks like this:

create_table "entries", :force => true do |t|
  t.string   "title"
  t.text     "content"
  t.datetime "created_at"
  t.datetime "updated_at"
end

You can use the select method to limit the fields that are returned like this:

class Entry < ActiveRecord::Base
  default_scope select([:id, :title])
end

In the rails console, you should see something like this:

puts Entry.where(:id => 1).to_sql  # => SELECT id, title FROM "entries"  WHERE "entries"."id" = 1

When you do want to select all of the fields, you can use the unscoped method like this:

puts Entry.unscoped.where(:id => 1).to_sql  # => SELECT * FROM "entries"  WHERE "entries"."id" = 1
phlipper
  • 1,834
  • 15
  • 14
  • This is not working with count. Entry.count.to_sql => # SELECT COUNT(id, title) FROM "entries" ... does not work in postgresql; Rails 5.2 – Stan Brajewski Jan 22 '20 at 08:44
2

As scoping, and default_scope mainly, gives many problems in use, my best way is to move big contents (binary, very large texts) to separate table.

create_table "entries", :force => true do |t|
  t.string   "title"
  # ... more small sized attributes of entries
  t.timestamps
end

create_table "entry_contents", :force => true do |t|
  t.references :entries, foreign_key: true
  t.text     "content"
  t.timestamps
end

class Entry ...
  # reference 
  has_one :entry_content

  # build entry_content for every new entry record
  after_initialize do |entry|
    entry.build_entry_content unless  entry.entry_content.present?
  end
end

This limits loading big data only when needed.

Entry.find(1).entry_content.content
Stan Brajewski
  • 452
  • 2
  • 5
1

To build on @phlipper answer's, if you want to just specify one or a few columns to get rid of:

class Entry < ActiveRecord::Base
   default_scope { select(Entry.column_names.map!(&:to_sym) - [:metadata]) }
end

As you can see, as of Rails 5+ you have to pass a block to default_scope.

Also, you should consider not using default scope

Pak
  • 2,123
  • 22
  • 28
  • True with not using default_scope. Also This "select" limitation is not working with count., Entry.count raises error SELECT COUNT(:id, :title, ...) ... FROM entries... "No function matches the given name and argument types. You might need to add explicit type casts."; Rails 5.2 – Stan Brajewski Jan 22 '20 at 08:42
0

Its not a default scope, but I am using the following solution for my case:

scope :no_content, -> { select(self.column_names.map(&:to_sym) - [:content]) }

Add no_content to the call is imo not a big deal, because you probably know which calls are a problem.

Brent Greeff
  • 174
  • 1
  • 6