Reset PK number based on association - ruby-on-rails

I have a Post and Comments table.
Post has many comments, and Comment belongs to a post.
I want to have primary keys which start at 1 when I create a comment for a Post, so that I can access comments in a REST-ful manner, e.g:
/posts/1/comments/1
/posts/1/comments/2
/posts/2/comments/1
/posts/2/comments/2
How can I achieve that with Rails 3?
I am using MySQL as a database.
Bonus: I am using the Sequel ORM; an approach compatible with Sequel, not only ActiveRecord, would be awesome.

Well, you can't use id for this, as id is a primary key here. What you can do is to add an extra field to your database table like comment_number and make it unique in the scope of the post:
#migration
def change
add_column :comments, :comment_number, :integer, null: false
add_index :comments, [:post_id, :comment_number], unique: true
end
#Class
class Comment < ActiveRecord::Base
belongs_to :post
validates :post_id, presence: true
validates :comment_number, uniqueness: { scope: :post_id }
end
Now with this in place you need to ensure this column is populated:
class Comment < ActiveRecord::Base
#...
before_create :assign_comment_number
private
def assign_comment_number
self.comment_number = (self.class.max(:comment_number) || 0) + 1
end
end
Last step is to tell rails to use this column instead of id. To do this you need to override to_param method:
class Comment < ActiveRecord::Base
#...
def to_param
comment_number
end
end
Update:
One more thing, it would be really useful to make this field read-only:
class Comment < ActiveRecord::Base
attr_readonly :comment_id
end
Also after rethinking having uniqueness validation on comment_number makes very little sense having it is assigned after validations are run. Most likely you should just get rid of it and rely on database index.
Even having this validation, there is still a possible condition race. I would probably override save method to handle constraint validation exception with retry a couple of time to ensure this won't break application flow. But this is a topic for another question.

Another option without changing models:
get 'posts/:id/comments/:comment_id', to: 'posts#get_comment'
And in the posts controller:
def get_comment
#comment = post.find(params[:id]).comments[params[:comment_id] -1]
end
Asumptions: Comments bookmarks might change if coments deletion is allowed.

Related

Make sure has_many :through association is unique on creation

If you are saving a has_many :through association at record creation time, how can you make sure the association has unique objects. Unique is defined by a custom set of attributes.
Considering:
class User < ActiveRecord::Base
has_many :user_roles
has_many :roles, through: :user_roles
before_validation :ensure_unique_roles
private
def ensure_unique_roles
# I thought the following would work:
self.roles = self.roles.to_a.uniq{|r| "#{r.project_id}-#{r.role_id}" }
# but the above results in duplicate, and is also kind of wonky because it goes through ActiveRecord assignment operator for an association (which is likely the cause of it not working correctly)
# I tried also:
self.user_roles = []
self.roles = self.roles.to_a.uniq{|r| "#{r.project_id}-#{r.role_id}" }
# but this is also wonky because it clears out the user roles which may have auxiliary data associated with them
end
end
What is the best way to validate the user_roles and roles are unique based on arbitrary conditions on an association?
The best way to do this, especially if you're using a relational db, is to create a unique multi-column index on user_roles.
add_index :user_roles, [:user_id, :role_id], unique: true
And then gracefully handle when the role addition fails:
class User < ActiveRecord::Base
def try_add_unique_role(role)
self.roles << role
rescue WhateverYourDbUniqueIndexExceptionIs
# handle gracefully somehow
# (return false, raise your own application exception, etc, etc)
end
end
Relational DBs are designed to guarantee referential integrity, so use it for exactly that. Any ruby/rails-only solution will have race conditions and/or be really inefficient.
If you want to provide user-friendly messaging and check "just in case", just go ahead and check:
already_has_role = UserRole.exists?(user: user, role: prospective_role_additions)
You'll still have to handle the potential exception when you try to persist role addition, though.
Just do a multi-field validation. Something like:
class UserRole < ActiveRecord::Base
validates :user_id,
:role_id,
:project_id,
presence: true
validates :user_id, uniqueness: { scope: [:project_id, :role_id] }
belongs_to :user, :project, :role
end
Something like that will ensure that a user can have only one role for a given project - if that's what you're looking for.
As mentioned by Kache, you probably also want to do a db-level index. The whole migration might look something like:
class AddIndexToUserRole < ActiveRecord::Migration
def change
add_index :user_roles, [:user_id, :role_id, :project_id], unique: true, name: :index_unique_field_combination
end
end
The name: argument is optional but can be handy in case the concatenation of the field names gets too long (and throws an error).

Ruby on Rails: How to iterate whether the user already commented on the post

This is my first question here in stackoverflow, so please bear with me hehe.
I have three models: User, Post, and Comments.
# user.rb
class User < ApplicationRecord
has_many :posts
has_many :comments
end
# post.rb
class Post < ApplicationRecord
belongs_to :user
has_many :comments
end
# comments.rb
class Comment < ApplicationRecord
belongs_to :user
belongs_to :post
end
What i'm trying to achieve is to let the user comment only once in a post. If there is already an existing comment by the same user, it should not have accept the save/create.
So far, my idea is to create a model function where it iterate all of the exisiting post.comments.each then check all of the users, then if there is, then the comment is invalidated. Though I have no Idea how to do it.
If you have any idea (and perhaps the code snippets), please do share. Thanks in advance! :)
In your comment.rb
validates :user_id, uniqueness: { scope: [:post_id]}
This validation will make sure the combination user_id and post_id will be unique in your comments table.
Well, there are a couple of ways to do it.
Firstly, it is possible to validate uniqueness of user_id - post_id combinations in comments:
# app/models/comments.rb
class Comment < ActiveRecord::Base
# ...
validates_uniqueness_of :user_name, scope: :account_id
Another approach is to manually check for comment existance before creating a comment:
if Comment.exists?(user_id: current_user.id, post_id: params[:comment][:post_id])
# render error
else
Comment.create(params[:comment])
end
But in a concurrent environment both approaches may fail. If your application is using a concurrent server like puma or unicorn you will also need a database constraint to prevent creation of duplicated records. The migration will be as follows:
add_index :comments, [:user_id, :post_id], unique: true

Rails initialize new object with defaults

I have the following class:
class Question < ActiveRecord::Base
attr_accessible :choices
end
I want to initialize new objects to have 4 blank strings in an array, so choices = ['','','','']. I've tried doing it in the controller:
def new
#question = Question.new(:choices => ['','','',''])
end
That works, but it seems like this should be done in the model to promote data integrity. Is there a better way to do this?
You have several solutions. In order of preference
Create a custom method and use it whenever you need such feature
class Question < ActiveRecord::Base
attr_accessible :choices
def self.prepare
new(:choices => ['','','',''])
end
end
Use the after_initialize callback
class Question < ActiveRecord::Base
attr_accessible :choices
after_initialize :default_choices
protected
def :default_choices
self.choices ||= ['','','','']
end
end
Override choices
I encourage the first approach for several reasons
You will have more control of the code. The defaults will be assigned only when you explicitly want it. In fact, there are several cases where an instance of that record is initialized (think about tests) and the majority of time that assignment may not be required
It's a good step towards exposing a custom API that is not tightly coupled to ActiveRecord
It exposes a more maintainable approach
You will love it if your app complexity will increase
Another solution:
4 Set as default in migration
class CreateQuestions < ActiveRecord::Migration
def change
create_table :questions do |t|
t.string :choices, default: ['', '', '', '']
t.timestamps
end
end
end
but I agree the first is the best solution.
In any case do not override the initialize http://blog.dalethatcher.com/2008/03/rails-dont-override-initialize-on.html, use the after_initialize callback described in the second solution.

How to inherit from models in Rails, where one type extends another without intertwining

I'm aware of the number of posts on this, but still I can't figure out how to do this. I have a model "InspirationItem", which is basically a blog posts. Now I also want a second model, "Special". Specials are like inspiration items but they have extra properties, such as an "excerpt" and a "theme". So I want to extend the "InspirationPost" model.
I've tried to create a model "Post", which both "InspirationItem" and "Special" extend, but "InspirationItem" doesn't really add any properties to. Then, I create a "has_one" relation from InspirationItem/Special and try to use "delegate" to handle all logics in the "Post" model. However this does not work like I'd expect at all.
Here's some of my code. This would be my InspirationItem:
class InspirationItem < ActiveRecord::Base
has_one :post, :as => :item
delegate :title, :title=,
:body, :body=,
:category_names, :category_names=,
:hide_from_overview, :hide_from_overview=,
:to => :post, :allow_nil => true
end
And this is a short version of post:
class Post < ActiveRecord::Base
attr_accessible :title, :body, :embed, :hide_from_overview, :visual, :thumbnail, :category_names
# All sorts of logics
end
What's important is that I don't want InspirationItem.all to return Specials too, that's why I use the Post model. I also want regular error handling to work for all models. Thanks in advance!
If you want an ActiveRecord subclass of a model, but don't want the parent to search any of the children, then something like this should work (I'll use your InspirationItem class):
class InspirationItem < ActiveRecord::Base
def self.descendants
super.reject {|klass| klass == Special}
end
end
class Special < InspirationItem
end
This is a bit hacky, but will force ActiveRecord to only return InspirationItems when you search InspirationItem.all. And this shouldn't affect validations.
EDIT: Re: What the tables would look like for this.
create_table :inspiration_items do |t|
t.string :type # needed for the Single Table Inheritance mechanism
# whatever other columns you need for InspirationItems
end

How can I invoke the after_save callback when using 'counter_cache'?

I have a model that has counter_cache enabled for an association:
class Post
belongs_to :author, :counter_cache => true
end
class Author
has_many :posts
end
I am also using a cache fragment for each 'author' and I want to expire that cache whenever #author.posts_count is updated since that value is showing in the UI. The problem is that the internals of counter_cache (increment_counter and decrement_counter) don't appear to invoke the callbacks on Author, so there's no way for me to know when it happens except to expire the cache from within a Post observer (or cache sweeper) which just doesn't seem as clean.
Any ideas?
I had a similar requirement to do something on a counter update, in my case I needed to do something if the counter_cache count exceeded a certain value, my solution was to override the update_counters method like so:
class Post < ApplicationRecord
belongs_to :author, :counter_cache => true
end
class Author < ApplicationRecord
has_many :posts
def self.update_counters(id, counters)
author = Author.find(id)
author.do_something! if author.posts_count + counters['posts_count'] >= some_value
super(id, counters) # continue on with the normal update_counters flow.
end
end
See update_counters documentation for more info.
I couldn't get it to work either. In the end, I gave up and wrote my own cache_counter-like method and call it from the after_save callback.
I ended up keeping the cache_counter as it was, but then forcing the cache expiry through the Post's after_create callback, like this:
class Post
belongs_to :author, :counter_cache => true
after_create :force_author_cache_expiry
def force_author_cache_expiry
author.force_cache_expiry!
end
end
class Author
has_many :posts
def force_cache_expiry!
notify :force_expire_cache
end
end
then force_expire_cache(author) is a method in my AuthorSweeper class that expires the cache fragment.
Well, I was having the same problem and ended up in your post, but I discovered that, since the "after_" and "before_" callbacks are public methods, you can do the following:
class Author < ActiveRecord::Base
has_many :posts
Post.after_create do
# Do whatever you want, but...
self.class == Post # Beware of this
end
end
I don't know how much standard is to do this, but the methods are public, so I guess is ok.
If you want to keep cache and models separated you can use Sweepers.
I also have requirement to watch counter's change. after digging rails source code, counter_column is changed via direct SQL update. In other words, it will not trigger any callback(in your case, it will not trigger any callback in Author model when Post update).
from rails source code, counter_column was also changed by after_update callback.
My approach is give rails's way up, update counter_column by myself:
class Post
belongs_to :author
after_update :update_author_posts_counter
def update_author_posts_counter
# need to update for both previous author and new author
# find_by will not raise exception if there isn't any record
author_was = Author.find_by(id: author_id_was)
if author_was
author_was.update_posts_count!
end
if author
author.update_posts_count!
end
end
end
class Author
has_many :posts
after_update :expires_cache, if: :posts_count_changed?
def expires_cache
# do whatever you want
end
def update_posts_count!
update(posts_count: posts.count)
end
end

Resources