Rails ActiveRecord - querying on last record of relationship - ruby-on-rails

I have a model Book with a has_many relationship to State.
class Book < ActiveRecord::Base
has_many :states
...
end
State sets the visibility of the book to "private", "restricted" or "public" through a name attribute. For auditing purposes, we keep a record of all state changes so to get the current state of a book I use
> book = Book.first
> book.states.last.name
> "public"
Now I need to query on all Book objects where the current state is public.
Something along the lines of:
Book.joins(:visibility_states).where(states: { state: "public"})
The problem is that the query above returns all books that are currently public, or have been public in the past. I only want it to return books that are currently "public" (i.e. book.states.last.name == "public").
I know I can do it with select but that's generating one query for each record:
Book.all.select { |b| b.states.last.name == "public" }
Is there a way to do it using ActiveRecord only?

You can use window function:
Book.joins(:visibility_states)
.where(states: { state: "public"})
.where("visibility_states.id = FIRST_VALUE(visibility_states.id)
OVER(PARTITION BY books.id ORDER BY visibility_states.created_at DESC))")
Or may be in your situation it would be better to save current state in Book model

I will do something with better performance.
If you want to save the historical state changes, is it ok. But try to avoid to introduce more complexity to your application because of this.
Why don't you add a current_state attribute in your Book model?
It will be much faster, and easier to develop.
class Book < ActiveRecord::Base
def set_new_current_state!(new_state)
self.current_state = new_state # e.g State.public
self.stats << State.create(new_state)
end
end
And your query will be just this:
Book.where(current_state: 'public')

Related

In a Rails 3 many to many association, what is the most efficient way to query objects based on conditions on their associations?

I have a many-to-many model relation:
class Movie
has_many :movie_genres
has_many :genres, :through => :movie_genres
class Genre
has_many :movie_genres
has_many :movies, :through => :movie_genres
class MovieGenre
belongs_to :movie
belongs_to :genre
I want to query all movies with a certain genre but not associated with another genre. Example: All movies that are Action but not Drama.
What I have done is this:
action_movies = Genre.find_by_name('action').movies
drama_movies = Genre.find_by_name('drama').movies
action_not_drama_movies = action_movies - drama_movies
Is there a more efficient way of doing this? It should be noted that the query can become more complex like: All movies that are Action but not Drama or All movies that are Romance and Comedy
You can indeed improve efficiency by avoid having to instantiate the Movie instances for all action and drama movies by removing the drama movies from the set of action movies in via the sql statement.
The basic building block is a dynamic scope similar to what widjajayd proposed
class Movie
...
# allows to be called with a string for single genres or an array for multiple genres
# e.g. "action" will result in SQL like `WHERE genres.name = 'action'`
# ["romance", "comedy"] will result in SQL like `WHERE genres.name IN ('romance', 'comedy')`
def self.of_genre(genres_names)
joins(:genres).where(genres: { name: genres_names })
end
...
end
You can use that scope as a building block to get the movies you want
All movies that are action but not drama:
Movie
.of_genre('action')
.where("movies.id NOT IN (#{Movie.of_genre('drama').to_sql}")
This will result in an sql subquery. Using a join would be nicer but it should be good enough for most cases and is a better read that the join alternative.
If your app where a rails 5 application you could even type
Movie
.of_genre('action')
.where.not(id: Movie.of_genre('drama'))
All movies that are Action but not Drama or All movies that are Romance and Comedy
Because it is a rails 3 app you will have to type move most of the sql by hand and can not make a lot of use of the scope. The or method is only introduced in rails 5. So this will mean having to type:
Movie
.joins(:genres)
.where("(genres.name = 'action' AND movies.id NOT IN (#{Movie.of_genre('drama').to_sql}) OR genres.name IN ('romance', 'comedy')" )
Again, if it where a rails 5 application this would be much simpler
Movie
.of_genre('action')
.where.not(id: Movie.of_genre('drama'))
.or(Movie.of_genre(['romance', 'comedy']))
probably using scope is better, here is sample and explanation (but not tested), create scope in movie model as follow
Movie.rb
scope :action_movies, joins(movie_genre: :genre).select('movies.*','genres.*').where('genres.name = ?', 'action')
scope :drama_movies, joins(movie_genre: :genre).select('movies.*','genres.*').where('genres.name = ?', 'drama')
in your controller, you can call as follow
#action_movies = Movie.action_movies
#drama_movies = Movie.drama_movies
#action_not_drama_movies = #action_movies - #drama_movies
edit for dynamic scope
if you want dynamic then you can send parameter to scope below is scope using block.
scope :get_movies, lambda { |genre_request|
joins(movie_genre: :genre).select('movies.*','genres.*').where('genres.name = ?', genre_request)
}
genre_request = parameter variable for scope
in your controller
#action_movies = Movie.get_movies('action')
#drama_movies = Movie.get_movies('drama')
Don't see a way to do it with one query (not without using subqueries anyway). But here is one that I think makes it a little better:
scope :by_genres, lambda { |genres|
genres = [genres] unless genres.is_a? Array
joins(:genres).where(genres: { name: genre }).uniq
}
scope :except_ids, lambda { |ids|
where("movies.id NOT IN (?)", ids)
}
scope :intersect_ids, lambda { |ids|
where("movies.id IN (?)", ids)
}
## all movies that are action but not drama
action_ids = Movie.by_genres("action").ids
drama_movies = Movie.by_genres("drama").except_ids(action_ids)
## all movies that are both action and dramas
action_ids = Movie.by_genres("action").ids
drama_movies = Movie.by_genres("drama").intersect_ids(action_ids)
## all movies that are either action or drama
action_or_drama_movies = Movie.by_genres(["action", "drama"])
It's possible to do except and intersect with raw SQL in Rails. But I think that's in general not a good idea as it still requires more than one query and also might make the code dependent on the DB used.
My original answer is rather naive. I'll leave it here so others won't make the same mistake:
Use joins and you can get it with one query:
Movie.joins(:genres).where("genres.name = ?", "action").where("genres.name != ?", "drama")
As noted in the comments, this will get all the movies that are both action and drama too.

How to validate uniqueness of a field without saving in Rails?

Suppose you have User has_many Books. Each book has a name field.
The user enters their books and it is submitted to the app as an array of names. The array of names will replace any existing books.
If the update fails, then the books should not be changed.
class Book
belongs_to :user
validates_uniquness_of :name, scope: [:user]
How to check the validity of each book without saving?
For example:
['Rails Guide', 'Javascript for Dummies'] would be valid.
['Javascript for Dummies', 'Javascript for Dummies'] would not be valid.
params[:books].each{| b | Book.new(b).valid? } will not work because the book has to be saves to get the uniqueness.
Mongoid
You can use an Active Record Transaction. Start the transaction, call save, and if it fails then the entire transaction will be rolled back. For example:
Book.transaction do
params[:books].each{ |b| Book.new(b).save! }
end
The entire transaction is aborted if there is an exception. You should handle this case by catching ActiveRecord::RecordInvalid.
You can use Array#map to convert array of books attributes to array of books names. Then use Array#uniq to remove duplicates from array of books names, and then check if resulting array has the same size as the original array of books attributes:
are_books_uniq = params[:books].map{|b| b[:name]}.uniq.size == params[:books].size
This way you can perform your check, without touching the database. But to be on a safe side, you should save all the books inside a transaction (see #Aaron's answer).
This turned out to be much more complicated than I imagined.
The solution I came up with looks like:
def update params
names = params.delete( :books )
new_books = names.map{| title | Book.new( name:name )}
validate_books_for new_books
return false if errors.present?
return false unless super( params )
self.books = new_books
self
end
Most of the complexity comes from the coupling of the 2 models. I can see why it is not a good idea to couple models. Perhaps a better design would be to store the books as an array.

Rails: eager load related model with condition at third model

I am very stuck on this problem:
I have next models (it's all from Redmine):
Issue (has_many :journals)
Journal (has_many :details, :class_name => "JournalDetails)
JournalDetails
I want fetch last changing of a state of a issue: date saved in Journal model but for filter only changing state I must join to JournalDetails.
Issue.eager_load(:journals).eager_load(journals: :details)
It's work but journals have all items not only changes of state - and how I can filter only changing of state without additional query I don't know
My next try:
Issue.eager_load({:journals => :details}).where("journal_details.prop_key = 'status_id'")
In this case I don't get issues which has not changes of state.
This should work:
Issue.joins(journals: :details).where(details: { prop_key: "status_id" })
Or, you can merge the scope from the details model:
class JournalDetails
scope :for_prop_key, -> (status_id) { where(prop_key: status_id )}
end
Issue.joins(journals: :details).merge(
JournalDetails.for_prop_key("status_id")
)
To fetch all issues, which have a non null prop_key in journal details:
class JournalDetails
scope :with_non_null_prop_key, -> { where("prop_key IS NOT NULL") }
end
Issue.joins(journals: :details).merge(
JournalDetails.with_non_null_prop_key("status_id")
)

Ruby on Rails: Activerecord collection associated models

I have two models as follows:
class Bookshelf < ActiveRecord::Base
has_many :books
scope :in_stock, -> { where(in_stock: true) }
end
class Book < ActiveRecord::Base
belongs_to :bookshelf
end
I would like to find all the books in a collection of bookshelves based on a column in the bookshelf table efficiently.
At the moment I have to loop through each member as follows:
available_bookshelves = Bookshelf.in_stock
This returns an activerecord relation
To retrieve all the books in the relation, i am looping through the relation as follows:
available_bookshelves.each do |this_bookshelf|
this_bookshelf.books.each do |this_book|
process_isbn this_book
end
end
I would like all the books from the query so that I don't have to loop through each "bookshelf" from the collection returned individually. This works but feels verbose. I have other parts of the app where similar queries-loops are being performed.
EDIT:
Some clarification: Is there a way to get all books in all bookshelves that fit a certain criteria?
For example, if there are 5 brown bookshelves, can we retrieve all the books in those bookshelves?
something like (this is not valid code)
brown_books = books where bookshelf is brown
You can use the following query to get the books in the in stock book shelves
available_books = Book.where(bookshelf_id: Bookshelf.in_stock.select(:id))
That will run a single query which will look like:
SELECT books.*
FROM books
WHERE books.bookshelf_id IN (SELECT id FROM bookshelves WHERE in_stock = true)

Using state attributes to maintain old records in Rails

I want to keep old records that would be normally destroyed. For example, an user joins a project, and is kicked from it later on. I want to keep the user_project record with something that flags the record as inactive. For this I use a state attribute in each model to define the current state of each record.
Almost all my "queries" want just the "active" records, the records with state == 1, and I want to use the ActiveRecord helpers (find_by etc). I don't want to add to all the "find_by's" I use a "_and_state" to find only the records that are active.
This is what I have now:
u = UserProject.find_by_user_id_and_project_id id1, id2
This is what I will have for every query like this for all models:
u = UserProject.find_by_user_id_and_project_id_and_state id1, id2, 1
What is the most cleaner way to implement this (the state maintenance and the cleaner query code)?
create a scope in your model UserProject:
class UserProject < ActiveRecord::Base
scope :active, where(:state => 1)
end
and "filter" your queries:
u = UserProject.active.find_by_user_id_and_project_id id1, id2
if you "almost allways" query the active UserProjects only, you can define this scope as default_scope and use unscoped if you want to query all records:
class UserProject < ActiveRecord::Base
default_scope where(:state => 1)
end
u = UserProject.find_by_user_id_and_project_id id1, id2 # only active UserProjects
u = UserProject.unscoped.find_by_user_id_and_project_id id1, id2 # all states
Here's a range of soft deletion gems you may want to choose from, which offer a nice abstraction that's already been thought through and debugged:
rails3_acts_as_paranoid
acts_as_archive
paranoia
Although if this happens to be your first Rails app, I second Martin's advice of rolling your own implementation.
I tried to just add this to Martin's answer, but my edit has to be reviewed, so even though Martin's answer was great, we can improve on it a little with the idea of default scopes. A default scope is always applied to finders on the model you add them to unless you specifically turn off the default scope:
class UserProject < ActiveRecord::Base
default_scope where(:state => 1)
end
The example Martin gave then becomes:
u = UserProject.find_by_user_id_and_project_id id1, id2
In this case, even without specifying that you want state == 1, you will only get active records. If this is almost always what you want, using a default scope will ensure you don't accidentally leave off the '.active' somewhere in your code, potentially creating a hard-to-find bug.
If you specify your default scope like this:
default_scope :conditions => {:state => 1}
then newly created UserProjects will already have state set to 1 without you having to explicitly set it.
Here's more information on default scopes: http://apidock.com/rails/ActiveRecord/Base/default_scope/class
Here's how to turn them off temporarily when you need to find all records:
http://apidock.com/rails/ActiveRecord/Scoping/Default/ClassMethods/unscoped

Resources