I'm trying to figure out the best way to build my model. Each user can have many balances, but I would like to enforce one balance of each currency per user. The application controls the record generation, so perhaps this is overkill. However, the question perplexed me, so I thought I'd ask the community.
If it makes sense to do, what would be the best way to build this?
My migration thus far:
class CreateBalances < ActiveRecord::Migration
def change
create_table :balances do |t|
t.decimal :amount
t.integer :currency, default: 0, null: false # this will be an enum in the model
t.references :user, index: true
t.timestamps
end
end
end
TL;DR: one of each :currency per :user
Try this in your Balance model:
validates :currency, :uniqueness => true, :scope => user_id
What I think this says (and I could be wrong, so please take this with a grain of salt) is, "Make sure that this type of currency exists only once for this user." Failing that, you could also try a custom validation:
validates :unique_currency_balance_per_user
def unique_currency_balance_per_user
Balance.where('id != ?', id)
.where(:currency => currency, :user_id => user_id)
.present?
end
There are also likely to be database-level constraints, but I am not yet aware of these.
Related
I have a user model and a Request model,
class Request < ActiveRecord::Base
belongs_to :user
validates :user_id, presence: true
validates :status, inclusion: { in: %w(PENDING
RUNNING
COMPLETED
FAILED
ERROR) }
validates :status, on: :create,
uniqueness: { scope: :user_id,
message: "There is another request for this user",
conditions: -> { where(status: ['PENDING',
'RUNNING',
'ERROR']) }
}
end
The idea behind the uniqueness validation is that you cannot create a Request for that user if he has another PENDING, RUNNING or ERROR request.
it 'cannot a new pending request if user allredy has another RUNNING task' do
described_class.create!(user: user, status: 'RUNNING')
expect { described_class.create!(user: user) }.to raise_error(ActiveRecord::RecordInvalid,
/There is another request for this user/)
end
This test fails because It can create another Request even when another RUNNING request was created.
I guess that is because I doing something wrong on the condition.
This is the migration that creates Request model.
class CreateRequests < ActiveRecord::Migration
def up
create_table :requests do |t|
t.integer :reason
t.string :status, default: 'PENDING', :limit => 15
t.integer :user_id
t.timestamps null: false
end
end
def down
drop_table :requests
end
end
Your issue is that you're trying to do too much with the validates ... uniqueness: built-in method. The method assumes that the column value must be unique, not unique within a collection.
(Your attempt to define this within the conditions parameter seems reasonable, but this only actually limits the scope of records being compared against; it does not alter the definition of "unique column" to mean "unique within a list".)
I would suggest instead writing this validation as a custom method - for example, something like:
validate :unique_status_within_collection, on: :create
def unique_status_within_collection
unique_statuses = ['PENDING', 'RUNNING', 'ERROR']
if unique_statuses.include?(status) && Request.exists?(user: user, status: unique_statuses)
errors.add(:status, "There is another request for this user")
end
end
I would solve this by using an ActiveRecord::Enum instead. First make requests.status an integer column with an index instead of varchar.
class CreateRequests < ActiveRecord::Migration
def change
create_table :requests do |t|
t.integer :reason
t.integer :status, default: 0, index: true
t.belongs_to :user, foreign_key: true
t.timestamps null: false
end
add_index :requests, [:user_id, :status]
end
end
You should also use t.belongs_to :user, foreign_key: true to setup a foreign key for the user association. Specifying up and down blocks is not needed - just specify the change block and ActiveRecord is smart enough to know how to reverse it on its own.
class Request < ActiveRecord::Base
# this defaults to true in Rails 5.
belongs_to :user, required: true
enum status: [:pending, :running, :completed, :failed, :error]
validate :unique_status_within_collection, on: :create
def unique_status_within_collection
unique_statuses = %w[ pending running error]
if unique_statuses.includes?(self.status) && user.requests.exists?(status: unique_statuses)
errors.add(:base, "There is another request for this user")
end
end
end
One huge gotcha though is that the name request already is really significant in rails and if you use the instance variable #request in the controller you're masking the incoming request object.
I would rename it something like "UserRequest" or whatever to avoid this ambiguity.
I've been trying to have my rails project only update the user table with the users unique facebook data. However, I can't get the facebook data to populate. I've tried multiple approaches but the end code seems to be hacky and using brute force to update the columns (as well as creating duplicate records)
Here are my examples:
User
class User < ActiveRecord::Base
has_one :facebook
def self.create_with_omniauth(auth)
create! do |user|
user.email = auth['email']
end
end
end
Facebook
class Facebook < ActiveRecord::Base
belongs_to :user
def self.create_with_omniauth(auth)
create! do |fb|
if auth['info']
fb.profile_link = auth['info']['profile_link'] || "test"
end
end
end
Migrations:
class CreateUsers < ActiveRecord::Migration
def change
create_table :users do |t|
t.string :email
t.timestamps null: false
end
end
end
class Facebooks < ActiveRecord::Migration
create_table :facebooks do |f|
f.belongs_to :user, index: true, :unique => true
f.string :profile_link
f.timestamps null: false
end
end
While creating the user:
SessionController (When calling create for user)
def create
auth = request.env["omniauth.auth"]
user = User.where(:provider => auth['provider'],
:uid => auth['uid'].to_s).first || User.create_with_omniauth(auth)
Facebook.create_with_omniauth(auth)
My understanding of Rails ActiveRecord so far... is that if I use "has_one" and "belongs_to" then it should automatically create records in the facebook table if a user table was created?
My expected Data would be:
SELECT * FROM users where id = 1;
id email
1 email#email.com
SELECT * FROM facebooks where user_id = 1;
id user_id profile_link
1 1 facebook.com/profile_link
facebook has no record created at all.
Not sure where I went wrong, I've followed tons of tutorials and hope I can master the active record.
Thanks!
Side Question for #val
def self.facebook_handler(user, auth)
if Facebook.exists?(user_id: id)
user = Facebook.find_by(user_id: id)
user.update(name: me['name'])
user.update(first_name: me['first_name'])
else
create! do |fb|
if me
fb.name = me['name']
fb.user_id = user.id
fb.first_name = me['first_name']
end
end
end
end
--- otherwise it kept inserting new records each time I logged in.
So many moving pieces in activerecord and in Rails. I think you have to go back to your migration and address a few things to set a solid model foundation for the view and controller parts of your MVC.
I see model-type function in the migration you posted, which is not going to serve you well. Migrations should be as flexible as possible, the constraints should be placed on the model.rb.
Migration: Flexible. Basic relationship indices set up.
Model: The
model.rb defines constraints (has_one, belongs_to, etc) and further
embellishes and validates data relationships (:dependent,:required,
etc.)
Your users model looks fine.
class CreateUsers < ActiveRecord::Migration
def change
create_table :users do |t|
t.string :email
t.timestamps null: false
end
end
end
Your facebooks migration should have looked more like this. Create a t.reference and add the index.
class Facebooks < ActiveRecord::Migration
create_table :facebooks do |f|
t.references :user, index: true
f.string :profile_link
f.timestamps null: false
end
add_index :facebooks, [:user_id]
end
Then in your Facebook model you can apply restraints and requirements
facebook.rb
belongs_to :user,
validates :user_id, presence: true, :unique => true
Your user model.rb should include:
has_one :facebook
There are some other questions about your higher level actions in the controller, but I think setting up your model will help you make progress towards your goal.
The model constraints below, along with the index setup looks like it would cause ActiveRecord to ROLLBACK and not add a duplicate facebook record for a given user. But it sounds like duplicates are being added to the facebook table. So, how?
facebook.rb
belongs_to :user,
validates :user_id, presence: true, :unique => true
...
user.rb
has_one :facebook
The 'if' clause you wrote looks to me as if it would be unnecessary if the relationship between user / facebook are set up and working in the model and database table, which makes me think there's a missing validation somewhere.
There's something to try, a model migration (change) on Facebook data description to add a :unique validator to the user_id field of the db table itself. (There's no change_index command, you have to remove and then add.)
remove_index :facebooks, [:user_d]
add_index :facebooks, [:user_id], :unique => true
Try taking your 'if' logic out and see if you're getting dupes. The relationships need to be properly setup before proceeding to the logic in the controller or you will break your head trying to unwind it.
And to your question in the comment, scopes are beautiful for creating collections based on parameters. So, in your user.rb model:
scope :important_thing_is_true, -> { where(:provider => auth['provider'],:uid => auth['uid'].to_s).first) }
Which is referenced by user.important_thing_is_true returns the collection or nil, which then you can test or use in other logic or display, etc. But, if you don't have the dupe records problem, maybe this scope isn't needed.
I have four models:
User
Award
Badge
GameWeek
The associations are as follows:
User has many awards.
Award belongs to user.
Badge has many awards.
Award belongs to badge.
User has many game_weeks.
GameWeek belongs to user.
GameWeek has many awards.
Award belongs to game_week.
Thus, user_id, badge_id and game_week_id are foreign keys in awards table.
Badge implements an STI model. Let's just say it has the following subclasses: BadgeA and BadgeB.
Some rules to note:
The game_week_id fk can be nil for BadgeA, but can't be nil for BadgeB.
Here are my questions:
For BadgeA, how do I write a validation that it can only be awarded one time? That is, the user can't have more than one -- ever.
For BadgeB, how do I write a validation that it can only be awarded one time per game week?
Data model:
In my comprehension, here is your data model (click to enlarge):
Data model http://yuml.me/6afcad62
Migration:
The migration will let you meet your second requirement, at the migration level:
class CreateAwards < ActiveRecord::Migration
def self.up
create_table :awards do |t|
# custom attributes here
t.string :name
t.text :description
t.references :user, :null => false
t.references :game_week#, :null => false
t.references :badge, :null => false
t.timestamps
end
# a user can be awarded no more than a badge per week
add_index :awards, [:user_id, :badge_id, :game_week_id], :unique => true
# a user can be awarded no more than a badge for ever
#add_index :awards, [:user_id, :badge_id], :unique => true
end
def self.down
drop_table :awards
end
end
Model:
The model will let you meet both your requirements, at the model level:
class Award < ActiveRecord::Base
validate_uniqueness_of :user, :badge,
:if => Proc.new { |award| award.badge === BadgeA }
validate_uniqueness_of :user, :badge, game_week,
:unless => Proc.new { |award| award.badge === BadgeA }
#validate_uniqueness_of :user, :badge, game_week,
# :if => Proc.new { |award| award.badge === BadgeB }
end
Note:
I didn't try these snippets, but I think that the idea is here :)
I'm trying to create default seed records for every user that signs up to the app. I'm thinking I could use the after_create method in my users observer model:
def after_create(user)
user.recipes.create(:name => "Sample Recipe", :description => "This is a sample recipe.")
user.cuisines.create(:name => "Sample Cusine", :description => "This is a sample cuisine.")
...
end
Is that too resource-intensive if I have 10 models that need seed data upon signup? Is there a more efficient way?
You're doing this the correct way, and here's why:
As business logic (every user should start with a sample cuisine and recipe) it belongs in the model.
This is where it is most easily testable.
If they have to be created for each user anyway, there's no less "resource intensive" way to do it. Any kind of batch process would leave the user without these defaults for a time.
Personally, I'd probably skip the added abstraction and complexity of putting it in the observer, because I'd want it obvious upon reading through the model that this is happening. But that's personal preference, and there's nothing wrong with how you've set it up here.
Why not set those as the default values in your database? That way there's no extra resources being used code-wise, it is one less point of failure, and you don't need to manually build up the samples in the associations. You can give a default to a column like this:
class AddRecipesDefaults < ActiveRecord::Migration
def self.up
change_column :recipes, :name, :string, :default => "Sample Recipe"
change_column :recipes, :default, :string, :default => "This is a sample."
end
def self.down
change_column :recipes, :name, :string, :default => nil
change_column :recipes, :name, :string, :default => nil
end
end
Suppose the following DB migration in Ruby:
create_table :question_votes do |t|
t.integer :user_id
t.integer :question_id
t.integer :vote
t.timestamps
end
Suppose further that I wish the rows in the DB contain unique (user_id, question_id) pairs. What is the right dust to put in the model to accomplish that?
validates_uniqueness_of :user_id, :question_id seems to simply make rows unique by user id, and unique by question id, instead of unique by the pair.
validates_uniqueness_of :user_id, :scope => [:question_id]
if you needed to include another column (or more), you can add that to the scope as well. Example:
validates_uniqueness_of :user_id, :scope => [:question_id, :some_third_column]
If using mysql, you can do it in the database using a unique index. It's something like:
add_index :question_votes, [:question_id, :user_id], :unique => true
This is going to raise an exception when you try to save a doubled-up combination of question_id/user_id, so you'll have to experiment and figure out which exception to catch and handle.
The best way is to use both, since rails isn't 100% reliable when uniqueness validation come thru.
You can use:
validates :user_id, uniqueness: { scope: :question_id }
and to be 100% on the safe side, add this validation on your db (MySQL ex)
add_index :question_votes, [:user_id, :question_id], unique: true
and then you can handle in your controller using:
rescue ActiveRecord::RecordNotUnique
So now you are 100% secure that you won't have a duplicated value :)
From RailsGuides. validates works too:
class QuestionVote < ActiveRecord::Base
validates :user_id, :uniqueness => { :scope => :question_id }
end
Except for writing your own validate method, the best you could do with validates_uniqueness_of is this:
validates_uniqueness_of :user_id, :scope => "question_id"
This will check that the user_id is unique within all rows with the same question_id as the record you are attempting to insert.
But that's not what you want.
I believe you're looking for the combination of :user_id and :question_id to be unique across the database.
In that case you need to do two things:
Write your own validate method.
Create a constraint in the database
because there's still a chance that
your app will process two records at
the same time.
When you are creating a new record, that doesn't work because the id of your parent model doesn't exist still at moment of validations.
This should to work for you.
class B < ActiveRecord::Base
has_many :ab
has_many :a, :through => :ab
end
class AB < ActiveRecord::Base
belongs_to :b
belongs_to :a
end
class A < ActiveRecord::Base
has_many :ab
has_many :b, :through => :ab
after_validation :validate_uniqueness_b
private
def validate_uniqueness_b
b_ids = ab.map(&:b_id)
unless b_ids.uniq.length.eql? b_ids.length
errors.add(:db, message: "no repeat b's")
end
end
end
In the above code I get all b_id of collection of parameters, then compare if the length between the unique values and obtained b_id are equals.
If are equals means that there are not repeat b_id.
Note: don't forget to add unique in your database's columns.