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?
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
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
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
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.
Related
It's hard to squeeze this whole question into a short title. I have a Records model, and each record has many :tags, and has many :words, through: :tags. Also, the Word table has a string column, holding the string form of the word.
I'm trying to build a search query, so that users can search for records with certain words and see all the words that each returned record has. However, so far when I have the queried Records only the words that I queried on are included. I need to show all the words (even the un-searched words) for each record. But I can't figure this out and it's been a week or two of reading SO questions and Rails docs. I've tried using plain rails ActiveRecord stuff, and I tried using Arel tables. I have one working solution, but that involves building an array of the plucked ids of all the found Records, and finding them again, which just puts a bad taste in my mouth. Anyway, here's what I have:
record.rb
class Record < ActiveRecord::Base
has_many :tags, dependent: :destroy
has_many :words, through: :tags
# this is the kind of query that I want
# +search+ is an array of words, ex: %w{chick fil a}
# however, `scope.first.words.count != scope.first.words(true).count`
# that is, the first record's words are not all force reloaded automatically
def self.that_have_all_words_in_rails(search)
scope = includes(:words)
search.each do |search_word|
scope = scope.where(words: { string: search_word })
end
scope
end
# I also tried this in arel, but it seems to give the same result as above
def self.that_have_all_words_in_arel(search)
scope = joins(:words)
search.each do |search_word|
scope = scope.where(word_table[:string].eq(search_word))
end
scope.includes(:words)
end
def word_table
Record.arel_table
end
# This is the only working version that I have, but it seems
# sloppy to use the record_ids array like this.
# That could be a 1000+ element array!
def self.that_have_all_words_in(search)
records = includes(:words)
record_ids = search.inject(pluck(:id)) do |ids, search_word|
ids & records.where(words: { string: search_word }).pluck(:id)
end
where(id: record_ids)
end
word.rb
class Word < ActiveRecord::Base
belongs_to :category
has_many :tags, dependent: :destroy
has_many :records, through: :tags
end
So, any ideas on how to be able to perform a query like:
Record.that_have_all_words_in(['chick', 'fil', 'a']).first.words
so that I get all the words of the first record, including words that are not 'chick' or 'fil' or 'a', without having to force reload with first.words(true)?
Thanks.
Update: the relevant parts of my schema
ActiveRecord::Schema.define(version: 20150727041434) do
create_table "records", force: true do |t|
t.datetime "created_at"
t.datetime "updated_at"
end
create_table "tags", force: true do |t|
t.integer "word_id"
t.integer "record_id"
t.datetime "created_at"
t.datetime "updated_at"
end
create_table "words", force: true do |t|
t.string "string"
t.datetime "created_at"
t.datetime "updated_at"
end
end
Also, I'm using sqlite3 for my db.
You could write a custom join with aliased tables to match the search conditions:
def self.where_tagged_words_match_strings(search)
search.uniq!
joins("INNER JOIN tags mtags ON mtags.record_id = records.id INNER JOIN words mwords ON mwords.id = mtags.word_id")
.where("mwords.string IN (?)", search).group("records.id").having("COUNT(DISTINCT mwords.string) = #{search.length}")
end
ETA This query should select records with words matching an arbitrary search array. It does this by selecting records whose words match any of the strings in the array, then grouping by records' id, selecting only those having a number of matching strings equal to the number of strings queried.
You can then chain this with includes(:words) to get all of the words associated, since the query above uses the aliased mwords:
Record.where_tagged_words_match_strings(search).includes(:words)
Relatedly, while all of the above should work in SQLite, I highly recommend that you switch to a more powerful and production-ready SQL database such as MySQL or PostgreSQL.
In a Rails ( 4.1.5 / ruby 2.0.0p481 / win64 ) application I have a many-to-many relationship between Student and Course and a join model StudentCourse which represents the association, which has an additional attribute called "started", which is set by default on "false".
I also have added an index in the join table made of the student_id and the course_id, and set a unique check on that, like this
t.index [:student_id, :course_id], :unique => true, :name => 'by_student_and_course'
Now I see that associations are created by either doing:
Student.first.courses.create(:name => "english")
or
Course.first.students << Student.first
This is fine and it's the expected behaviour, I suppose.
What I am looking after is the correct way to get and set the "started" attribute.
I am seeing an odd behaviour when accessing that attribute from the other models and not straight from the join model.
s = Student.create
c = Course.create(:name => "english")
s.student_courses.first
=> | "english" | false | # (represented as a table for practicity)
s.student_courses.first.started = true
=> | "english" | true |
s.save
=> true
Ok this looks like it has been saved but when I loot ak:
StudentCourse.first
=> | 1 | 1 | false |
So it is set on true if I go through the student nested attributes, but it's still false in the join model. I also tried doing "reload!" but it makes no difference and they will mantaint their own different value.
If something is going so bad that values are not actually persisted I should be told instead of getting "true" when saving, because otherwise how bad could be the consequences of this ? What am I missing here?
Anyway, if I try modifying the "started" attribute on the join model directly, I meet another kind of problem:
StudentCourse.first.started = true
StudentCourse Load (1.0ms) SELECT "student_courses".* FROM "student_courses" LIMIT 1
=> true
StudentCourse.first.started
=> false
It has not changed!
StudentCourse.find_by(:student_id => "10", :course_id => "1").started = true
=> true
StudentCourse.find_by(:student_id => "10", :course_id => "1").started
=> false
Same as before.. I try with:
StudentCourse.find(1).started = true
ActiveRecord::UnknownPrimaryKey: Unknown primary key for table student_courses in model StudentCourse.
Then with:
sc = StudentCourse.first
sc.started = true
=> true
sc
=> | 1 | 1 | true |
seems great but when saving:
sc.save
(0.0ms) begin transaction
SQL (1.0ms) UPDATE "student_courses" SET "started" = ? WHERE
"student_courses"."" IS NULL [["started", "true"]]
SQLite3::SQLException: no such column: student_courses.: UPDATE
"student_courses" SET "started" = ? WHERE "student_courses"."" IS NULL
(1.0ms) rollback transaction ActiveRecord::StatementInvalid:
SQLite3::SQLException: no such column: student_courses.: UPDATE
"student_courses" SET "started" = ? WHERE "student_courses"."" IS
NULL from
C:/Ruby200-x64/lib/ruby/gems/2.0.0/gems/sqlite3-1.3.9-x64-mingw32/lib/sqlite3/database.rb:91:in
`initialize'
So I think this all has to do with not having a primary key in
join-table?
But I am not sure enough on how to use it and if that'd represent a
good practice for the case I am trying to solve ?
Also, if this is the problem, why then I don't get the same warning
here when I save the student after I do
s.student_courses.first.started = true, as shown in the examples
above?
Code
student.rb
class Student < ActiveRecord::Base
has_many :student_courses
has_many :courses, :through => :student_courses
end
course.rb
class Course < ActiveRecord::Base
has_many :student_courses
has_many :students, :through => :student_courses
end
student_course.rb
class StudentCourse < ActiveRecord::Base
belongs_to :course
belongs_to :student
end
schema.rb
ActiveRecord::Schema.define(version: 20141020135702) do
create_table "student_courses", id: false, force: true do |t|
t.integer "course_id", null: false
t.integer "student_id", null: false
t.string "started", limit: 8, default: "pending", null: false
end
add_index "student_courses", ["course_id", "student_id"], name: "by_course_and_student", unique: true
create_table "courses", force: true do |t|
t.string "name", limit: 50, null: false
t.datetime "created_at"
t.datetime "updated_at"
end
create_table "students", force: true do |t|
t.string "name", limit: 50, null: false
t.datetime "created_at"
t.datetime "updated_at"
end
end
create_join_table.rb (migration for join table)
class CreateJoinTable < ActiveRecord::Migration
def change
create_join_table :courses, :students, table_name: :student_courses do |t|
t.index [:course_id, :student_id], :unique => true, :name => 'by_course_and_student'
t.boolean :started, :null => false, :default => false
end
end
end
Ok I finally got what was going on here:
If you create a join table in a migration using #create_join_table, this method will not create the default primary key called "id" (and not add an index for it) which is what rails does by default when using #create_table.
ActiveRecord needs a primary key to build its queries, because it is the column that it will be used by default when doing things like Model.find(3).
Also if you think you can get around this by doing something like StudentCourse.find_by(:course_id => "1", :student_id => "2").update_attributes(:started => true) [0] it will still fail, because after the record it's found, AR will still try to update it looking at the "id" of the record it found.
Also StudentCourse.find_by(:course_id => "1", :student_id => "2").started = true will retrun true but of course it is not saved until you call #save on it. If you assign it to a var relationship and then you call relationship.save you will see it will fail to save for the above reasons.
[0]
In the join table I didn't want duplicate records for a "student_id" and "course_id" so in the migration I had explicitely added a unique constraint for them (using unique index).
This led me to think that I did not need anymore a primary key to uniquely identify a record, because I had those two values... I thought that adding an index on them was enough for they to work as a primary key... but it is not. You need to explicitely define a primary-key when you are not using the default "id" one.
Also turns out that Rails does not support composite primary keys and so even if I wanted to add a primary key build on those two values (so making them primary-key and unique-index, like default rails "id" works) it would have not been possible.
A gem for that exists: https://github.com/composite-primary-keys/composite_primary_keys
So, end of the story, the way I fixed it was simply adding t.column :id, :primary_key to the migration for the join table creation. Also I could have not created the join table with #create_join_table but instead using just #create_table (which would create an "id" automatically").
Hope this helps someone else.
Also this answer to another question was very helpful, thank you #Peter Alfvin !
OK, it appears that you don't have a primary key (we are getting confirmation shortly) in your join table. You do need to have a primary key when trying to access the join table.
I would suggest your migration be:
class CreateStudentCourses < ActiveRecord::Migration
def change
create_table :student_courses do |t|
t.references :course
t.references :student
t.boolean :started, default: false
t.timestamps
t.index [:student_id, :course_id], :unique => true, :name => 'by_student_and_course'
end
end
end
The model definitions look good, so that would be the only change I can see that needs to be made.
After that, doing what you have been doing should work correctly. You would create the join and then access it after the creation. If you want to assign the boolean to true upon creation, you would need to create the record through the StudentCourse model with the information you need (student_id, course_id and started = true) instead of through either association.
StudentCourse.create(course_id: course.id, student_id: student.id, started: true)
s = Student.create
c = Course.create(:name => "english")
s.student_courses.first.started = true
s.save
I think the clue here is in the first line that you posted (represented above). s is an instance of the student and when you call s.save then you're asking the student to save any changes to its attributes. There are not any changes to save, however, because you made a change to an association.
You have a couple of options. If you prefer the direct access approach from your code snippet then the following should work.
s = Student.create
c = Course.create(:name => 'english')
s.courses << c
s.student_courses.first.update_attributes(:started => true)
Another alternative would be to use the accepts_nested_attributes_for macro to expose the started attribute from the student perspective.
class Student
has_many :student_courses, :inverse_of => :student
has_many :courses, :through => :student_courses
accepts_nested_attributes_for :student_courses
end
s = Student.create
c = Course.create(:name => 'english')
s.courses << c
s.update_attributes(:student_courses_attributes=>[{:id => 1, :started => true}])
I need some advice on a voting system in rails that recognizes the top vote getter on a monthly basis. I have a system that works but being new to rails, I'm sure there are more efficient methods available. Below is a simplified version of my current setup(controller code omitted):
class Charity < ActiveRecord::Base
has_many :votes
end
class Vote < ActiveRecord::Base
belongs_to :charity
end
My schema is as follows:
ActiveRecord::Schema.define(:version => 20130310015627) do
create_table "charities", :force => true do |t|
t.string "name"
t.text "description"
t.date "last_win"
t.datetime "created_at", :null => false
t.datetime "updated_at", :null => false
end
create_table "votes", :force => true do |t|
t.integer "charity_id"
t.datetime "created_at", :null => false
t.datetime "updated_at", :null => false
end
end
I'll be using the 'whenever' gem to run a cron job to determine the monthly winner and update the 'last_win' column of the charities table.
The following code is where I'm questioning my efficiency:
vote_counts = Vote.count(:group => "charity_id")
most_votes = vote_counts.values.max
winning_ids = vote_counts.map{|k,v| v == most_votes ? k :nil }.compact
charities = Charity.find(winning_ids)
charities.each {|charity| charity.update_attributes(:last_win => Date.today)}
I'm sure there are many ways to do this better and would appreciate some suggestions. If you have suggestions on better ways to set up the votes table / associations, that would be appreciated too.
Thanks in advance,
CRS
Something like this:
If there was only one winner, this would work I think
winner_id = Vote.group(:charity_id).order("count(*) desc").pluck(:charity_id).first
Charity.find(winner)id).update_attribute!(:last_win => Date.today)
You could modify it for ties:
most_votes = Vote.group(:charity_id).order("count(*) desc").count.first[1]
winners = Vote.group(:charity_id).having("count(*) = ?", most_votes).pluck(:charity_id)
Charity.where(:id => winners).update_all(:last_win => Date.today)
Make sure everything is indexed correctly in your database,
You can probably streamline it more, but the SQL is going to get more complicated.
The last two lines could be:
Charity.where(id:winning_ids).update_all(last_win:Date.today)
Which would translate into a single SQL update command, instead of issuing an update command for each winning charity.
The first part where you identify the winning charities looks okay, and since you're running it as a cron job you probably don't care if it takes a few minutes.
However, if you'd like to show the values in real time, you could add an after_create hook on Vote to update a counter for its owner charity (possibly in another table):
class Vote < ActiveRecord::Base
belongs_to :charity
after_create :increment_vote_count
CharityVote.where(year:Time.now.year, month:Time.now.month,
charity_id:self.charity_id).first_or_create.increment!(:counter)
end
I have the following DB for a simple flash cards example i'm building:
create_table "card_associations", :force => true do |t|
t.integer "card_id"
t.integer "deck_id"
end
create_table "cards", :force => true do |t|
t.string "question"
t.string "answer"
end
create_table "decks", :force => true do |t|
t.string "name"
t.string "description"
end
I've setup the has_many through relationships in all my models.
Now I want to be able to return a list of all cards from the join table, given the deck id.
If I run the query:
CardAssociation.find_by_deck_id(3).card
It retruns the first card with the deck_id of 3. But when I try.
CardAssociation.find_all_by_deck_id(3).card
I get the error
NoMethodError: undefined method `card' for #
Can someone help me with this? I feel like i'm making a very simple mistake.
Thanks for the help
The find_all_* methods always return an Array (which could be empty)!
CardAssociation.find_all_by_deck_id(3) # => Array of results
CardAssociation.find_all_by_deck_id(3).first # => first result of the Array or nil if no result
I advise you to first read the Ruby on Rails Style Guide, and then use the Rails3 way of finding object with ActiveRecord:
CardAssociation.where(:deck_id => 3) # => Array of results
CardAssociation.where(:deck_id => 3).first # => first result of the Array if exists
In your case, a scope can be set up on the Card model:
You said: "Now I want to be able to return a list of all cards from the join table, given the deck id"
class Card < ActiveRecord::Base
scope :for_deck, lambda { |deck| joins(:card_associations).where('card_associations.deck_id = ?', deck.try(:id) || deck) }
end
This scope can be used like following:
Card.for_deck(deck) # returns an Array of Card objects matching the deck.id
As defined in the scope, the parameter of Card.for_deck(deck) can be a deck object or a deck_id (type Integer)
Hope this helped!
I'm working through the RailsTutorial but making an "Announcements" webapp for the middle school I teach at (tweaking the given Twitter clone).
When users create an announcement, they use check boxes to determine which grades it should be displayed to (1-3 grades could be true). This is working correctly, with me storing grades as booleans.
create_table "announcements", :force => true do |t|
t.string "content"
t.integer "user_id"
t.boolean "grade_6"
t.boolean "grade_7"
t.boolean "grade_8"
t.date "start_date"
t.date "end_date"
t.datetime "created_at"
t.datetime "updated_at"
end
My users also have a grade field, which is an integer. I want to use this to make each user's home page show the announcements for their grade.
Example: An 8th grade teacher has grade = 8. When they log in, their home page should only show announcements which have grade_8 = TRUE.
Example: An principal has grade = 0. When they log in, their home page should show all announcements.
I'm struggling with how to translate the integer user.grade value into boolean flags for pulling announcements from the model.
The code I'm writing is working, but incredibly clunky. Please help me make something more elegant! I'm not tied to this db model, if you have a better idea. (In fact, I really don't like this db model as I'm hardcoding the number of grades in a number of locations).
# Code to pull announcements for the home page
def feed
case grade
when 6
grade_6
...
else
grade_all
end
end
# Example function to pull announcements for a grade
def grade_6
Announcement.where("grade_6 = ? AND start_date >= ? AND end_date <= ?",
TRUE, Date.current, Date.current)
the correct way to set this type of relationship up would be to use a many-to-many relationship via has_many through:
class Announcement < ActiveRecord::Base
has_many :announcement_grades
has_many :grades, :through => :announcement_grades
end
class AnnouncementGrades < ActiveRecord::Base
belongs_to :grade
belongs_to :announcement
end
class Grade < ActiveRecord::Base
has_many :announcement_grades
has_many :announcements, :through => :announcement_grades
end
then your migrations will be:
create_table :announcements, :force => true do |t|
t.date :start_date
t.date :end_date
t.timestamps #handy function to get created_at/updated_at
end
create_table :announcement_grades, :force => true do |t|
t.integer :grade_id
t.integer :announcement_id
t.timestamps
#start and end date might be more appropriate here so that you can control when to start and stop a particular announcement by grade rather than the whole announcement globally, depending on your needs.
end
create_table :grades, :force => true do |t|
t.timestamps
#now you have a bona-fide grade object, so you can store other attributes of the grade or create a relationship to teachers, or something like that
end
so, now you can simply find your grade then call announcements to filter:
#grade = Grade.find(params[:id])
#announcements = #grade.announcements
so, that's the correct way to do it from a modeling perspective. there are other considerations to this refactor as you will have to make significant changes to your forms and controllers to support this paradigm, but this will also allow for much greater flexibility and robustness if you decide you want to attach other types of objects to a grade besides just announcements. this railscast demonstrates how to manage more than one model through a single form using nested form elements, this will help you keep the look and feel the same after you apply the changes to your models. I hope this helps, let me know if you need more help doing this, it'll be a bit of work, but well worth it in the end.
Chris's example is theoretically superior. However, your original schema may be more practical if 1) you know your app won't become more complicated, and 2) the US's k-12 system is here to stay (i would bet on it...). If you would prefer to stick with the schema that you already have, here some improvements you could make to the code:
Let's add a 'grade' scope to your Announcement model
class Announcement < ActiveRecord::Base
....
scope :grade, lambda do |num|
num > 0 ? where("grade_#{num} = ?", true) : where('1=1')
end
....
end
This would allow for much simpler coding such as
teacher = User.find(user_id)
announcements = Announcement.grade(teacher.grade).where('start_date >= :today AND end_date <= :today', {:today => Date.today})