Good afternoon. I'm new to rails and I'm using google translate to post in English here, so sorry if it's not very readable.
My question is, I have a User table, and a Setting table.
They are related (but I don't know if the relationship is correct), they can even confirm me, and I would like to know if:
when creating a user, I would like to automatically change the "email" and "push" fields of that user's settings table to true.
Would it be possible via a method that in the user model called: "setting_default"?
User model.
class User < ApplicationRecord
has_one :setting
before_save :setting_default
def setting_default
self.setting.update(:email, 'true')
self.setting.update(:push, 'true')
end
Setting Model
class Setting < ApplicationRecord
has_one :user
end
The Controller is normal, if you need it, I can put it in the post
My migration:
class CreateSettings < ActiveRecord::Migration[6.0]
def change
create_table :settings do |t|
t.boolean :email, default: true
t.boolean :push, default: true
t.timestamps
end
end
end
class AddSettingsToUser < ActiveRecord::Migration[6.0]
def change
add_reference :users, :setting, null: true, foreign_key: true
end
end
Google translate has worked well for you here.
First off you'll want to change your Setting model to belong to the User:
class Setting < ApplicationRecord
belongs_to :user
end
Your settings DB table is missing a user_id field to tie the setting back to the user. I'm not used to the add_reference technique so I just do things myself in the migrations. This would work:
class CreateSettings < ActiveRecord::Migration[6.0]
def change
create_table :settings do |t|
t.integer :user_id
t.boolean :email, default: true
t.boolean :push, default: true
t.timestamps
end
end
end
(Make note that your users DB table has a field setting_id that it does not need. I don't think it should be there. I would remove it. Unless it's a Rails 6 thing I'm not used to.)
Next it would probably be better to assign the values if the save succeeds (and not if it fails) so you'll want an after_save instead. And I'm simplifying your value assignment just in case you're having an issue there:
class User < ApplicationRecord
has_one :setting
after_save :setting_default
def setting_default
setting.email = true
setting.push = true
setting.save(validate: false)
end
private :setting_default
And to answer what seems to be your question, yes, what you're trying to do should be easily possible. This is a very common thing to do. It should work.
When you use one-to-one association you need to choose has_one in one and belongs_to in another model
Semantically user has one setting, but not setting has one user
So it's better to reverse them
To change your schema you need to write new migration
class ChangeOneToOneDirection < ActiveRecord::Migration[6.0]
def up
change_table :settings do |t|
t.belongs_to :user, foreign_key: true, null: false
end
User.where.not(setting_id: nil).find_each |user|
Setting.find(user.setting_id).update_columns(user_id: user.id)
end
change_table :users do |t|
t.remove :setting_id
end
end
def down
add_reference :users, :setting, null: true, foreign_key: true
Setting.find_each do |setting|
User.find(setting.user_id).update_columns(setting_id: setting.id)
end
change_table :settings do |t|
t.remove :user_id
end
end
end
After migration you can change User model
class User < ApplicationRecord
has_one :setting
after_commit :setting_default
private
def setting_default
setting&.update(email: true, push: true)
end
end
It's better to update associated model only if saves are in the database. And user can haven't setting. That's why after_commit and safe-navigator &
Related
I would like to add a review section to my app. To be more specific, a user can leave a review for a shop and the shop can then reply to that review. But I'm not sure if the model associations and review table migrations I have are correct.
class User < ActiveRecord::Base
has_many :reviews
end
class Review < ActiveRecord::Base
belongs_to :user
end
class ReviewReply < ApplicationRecord
belongs_to :user, optional: true
belongs_to :review, optional: true
end
class Shop < ActiveRecord::Base
has_many :reviews
end
class CreateReviews < ActiveRecord::Migration[6.0]
def change
create_table :reviews do |t|
t.text :body
t.integer :rating
t.references :shop, null: false, foreign_key: true
t.references :user, null: false, foreign_key: true
t.timestamps
end
end
end
Architecture:
1. CALCULATING RATING
console:
rails g migration add_rating
migration:
def change
add_column :shops, :average_rating, :integer, default: 0, null: false
add_column :reviews, :rating, :integer, default: 0, null: false
end
user.rb
has_many :reviews
shop.rb
has_many :reviews
def update_rating
if reviews.any? && reviews.where.not(rating: nil).any?
update_column :average_rating, reviews.average(:rating).round(2).to_f
else
update_column :average_rating, 0
end
end
review.rb
belongs_to :user
belongs_to :shop
has_one :review_reply
after_save do
unless rating.nil? || rating.zero?
shop.update_rating
end
end
after_destroy do
shop.update_rating
end
review_reply.rb
belongs_to :review
2. REPLY TO A REVIEW
views/reviews/show.html.erb:
<% unless #review.review_reply.present? %>
<%= link_to "Write a Reply", new_review_reply_path(review_id: #review.id) %>
<% end %>
views/review_replies/form.html.erb
= f.input :review_id, input_html: {value: params[:review_id]}, as: :hidden
There are a couple of things things that I would change if it's a real application (= not just something you're toying around with):
I'd remove the optional: true from the two associations in ReviewReply since replies don't make sense (data-wise) if they don't have an author or aren't connected to a review.
I'd also set these two columns in review_replies to null: false.
You should think about adding some deletion cascades (either adding dependent: :some_action in models or using on_delete: :some_action on the database columns – I'd recommend the latter):
delete review replies when a review is deleted?
delete review replies when a user is deleted? (or just set it to NULL and then show "Deleted User" in the UI?)
delete reviews when a shop is deleted?
delete reviews when a user is deleted? (or just set it to NULL and then show "Deleted User" in the UI?)
In your migration polymorphic relation for reviewable is set up incorrectly - unique index on reviewable_type will prevent adding multiple records, better use
t.references :reviewable, polymorphic: true
in modern rails it adds non-unique index on both columns by default (you can be explicit with index: true, but in any way this is different from two separate indexes on each column alone)
Also most probably you want your unique index to include user id so that each user can review each reviewable once:
t.index [:reviewable_type, :reviewable_id, :user_id], unique: true, name: 'idx_unique_user_review'
Reason for including both shop and reviewable is not clear, but it depends on your application and goals. If the shop itself is the reviewable (as suggested by has_many :reviews, as: :reviewable) - then shop reference is useless. But in fact if you do not plan on extending reviews on something other than shops - it's easier to go with non-polymorphic reference for now.
In large app it's better to have common name prefixes for related things, so ReviewReply model name is better. belongs_to :review is most probably not optional, it's very strange to reply to nothing. Also it will most likely have belongs_to :shop (also not optional) not user, since the shop is the one replying.
I'm building a tech-specific pricegrabber-like web app, and I have a model that carries params that are common in all products. This model is called Product. Then I have one model for each type of product that I'm going to work with, for example, I'm now trying to build the first specific model, which is Videocard. So, the Product model always must have one Specific model, in this case Product-Videocard.
At this moment I'm stuck finding a way to make a product and a specific model always come tied together whenever I reach to them, be it in an index view, show view, form_for, a search, etc. But I can't picture in my head how a form will create an item and its specifications and insert a foreign key into another model with only one submit request.
Below are both models and the migrations for each:
class Product < ApplicationRecord
#belongs_to :productable, :polymorphic => true
has_one :videocard, dependent: :destroy
# Comment for this Stackoverflow question: the way I'm thinking I
# should have to make tons of has_one associations, for the other
# products. Is there a DRY way to do this?
has_many :prices, through: :stores
validates :platform, presence: { message: "should be specified." }
validates :name, presence: { message: "should be specified." }
validates_associated :videocard
end
class Videocard < ApplicationRecord
belongs_to :product
end
Migrations (shortened to make this question as clear as possible):
class CreateProducts < ActiveRecord::Migration[5.0]
def change
create_table :products do |t|
t.references :productable, polymorphic: true, index: true
t.string :name
t.string :image
t.string :partnum
t.string :manufacturer
t.string :platform #mobile, desktop, server, laptop
t.timestamps
end
end
end
class CreateVideocards < ActiveRecord::Migration[5.0]
def change
create_table :videocards do |t|
t.references :product, index: true
t.integer :memory
t.string :interface
# [...lots of attributes...]
t.integer :displayport
t.integer :minidisplayport
t.integer :tdp
t.timestamps
end
end
end
Also how can I make it so that Product only needs one has_one association, instead of using multiple ones. Remember that Videocard will have one type of specification, Memory will have other, and so on.
I am using rails 4.2, I just want to know if there would be any difference if I use the :foreign_key keyword in my migrations rather than just adding a user_id column to add relationship to my models ?
YES
The key difference is not on the application layer but on the database layer - foreign keys are used to make the database enforce referential integrity.
Lets look at an example:
class User < ActiveRecord::Base
has_many :things
end
class Thing < ActiveRecord::Base
belongs_to :user
end
If we declare things.user_id without a foreign key:
class CreateThings < ActiveRecord::Migration
def change
create_table :things do |t|
t.integer :user_id
t.timestamps null: false
end
end
end
ActiveRecord will happily allow us to orphan rows on the things table:
user = User.create(name: 'Max')
thing = user.things.create
user.destroy
thing.user.name # Boom! - 'undefined method :name for NilClass'
While if we had a foreign key the database would not allow us to destroy user since it leaves an orphaned record.
class CreateThings < ActiveRecord::Migration
def change
create_table :things do |t|
t.belongs_to :user, index: true, foreign_key: true
t.timestamps null: false
end
end
end
user = User.create(name: 'Max')
thing = user.things.create
user.destroy # DB says hell no
While you can simply regulate this with callbacks having the DB enforce referential integrity is usually a good idea.
# using a callback to remove associated records first
class User < ActiveRecord::Base
has_many :things, dependent: :destroy
end
Our Rails app works with the following models:
class User < ActiveRecord::Base
has_many :administrations, dependent: :destroy
has_many :calendars, through: :administrations
end
class Administration < ActiveRecord::Base
belongs_to :user
belongs_to :calendar
end
class Calendar < ActiveRecord::Base
has_many :administrations, dependent: :destroy
has_many :users, through: :administrations
end
And here are our migrations:
class CreateUsers < ActiveRecord::Migration
def change
create_table :users do |t|
t.string :first_name
t.string :last_name
t.string :email
...
t.integer :total_calendar_count
t.integer :owned_calendar_count
t.timestamps null: false
end
end
end
class CreateAdministrations < ActiveRecord::Migration
def change
create_table :administrations do |t|
t.references :user, index: true, foreign_key: true
t.references :calendar, index: true, foreign_key: true
t.string :role
t.timestamps null: false
end
end
end
class CreateCalendars < ActiveRecord::Migration
def change
create_table :calendars do |t|
t.string :name
t.timestamps null: false
end
end
end
When a new #calendar is created, we need to increment :total_calendar_count and :owner_calendar_count by one in the User table.
We tried this in the CalendarsController:
class CalendarsController < ApplicationController
def create
#calendar = current_user.calendars.create(calendar_params)
current_user.total_calendar_count += 1
current_user.owned_calendar_count += 1
current_user.administrations.find_by(calendar_id: #calendar.id).update(role: 'Creator')
...
end
But it does not seem to update :total_calendar_count and :owner_calendar_count by one in the User table.
Are we missing a step here? Should we use an update action instead?
The actual problem in your code is that you don't then save the user.
So you update the counter... but this changes it on the local instance... and then after the controller action is done the change you made just disappears.
if you wanted to keep your code the way it is, you could do:
current_user.save
at the end.
but I'd advise you to look into the counter_cache, because it's the Rails way.
Also I'll point out that you haven't checked that the calendar successfully got created, before incrementing that counter... it's possible that it could fail a validation and not really have been created... you need to check for that first.
I have a best idea to solve your problems is as below....
Create a method that will call on the creating of calendar with the callbacks of model like as below...
Add the below inside the calendar model just after the validation and ORM relations
after_create :increment_counter
def increment_counter
calendar_user = self.user
calendar_user.update(:total_calendar_count += 1, :owned_calendar_count += 1 )
end
With the above code you don't need to do anything. It will increment the counter of calendar on every new entry of calendar.
In the console
a = Reported.new
This works. After tinkering.
a.profile = Profile.first
But it's not what I want! I want a.profile to even exist. I want a.reported_by to be a profile! And I want a.reported to be a profile!
Again what I want is
a.reported_by = Profile.last #or any such profile
a.reported = Profile.first #or any such profile
Model
class Profile < ActiveRecord::Base
has_many :reported, dependent: :destroy
Migration
It doesn't have a reported column, I am not sure about the right way to implement that either.
class CreateReporteds < ActiveRecord::Migration
def change
create_table :reporteds do |t|
t.belongs_to :profile
t.integer :reported_by
t.string :reason
t.timestamps
end
end
end
Your migration seems... off. I've never seen t.belongs_to :something in a migration - shouldn't it be t.integer :profile_id? (I couldn't find documentation supporting the belongs_to syntax there).
If you want Reported#reported_by to return a Profile, then you need a reported_by_id integer on it, NOT a reported_by integer. Rails has a convention where you should make your referenced objects (in this case, a belongs_to :reported_by relationship) use the relationship_id format for it's foreign key.
Then you should have this in your class:
class Reported < ActiveRecord::Base
belongs_to :reported_by, class_name: "Profile"
end
This will make it so it uses reported_by_id as the foreign key for a Profile object, but return it as Reported#reported_by.
Then:
class Profile < ActiveRecord::Base
has_many :reporteds, foreign_key: 'reported_by_id'
end
Should let you do Profile#reporteds
And your migration would look like this:
class CreateReporteds < ActiveRecord::Migration
def change
create_table :reporteds do |t|
t.integer :reported_by_id
t.string :reason
t.timestamps
end
end
end