I'm in the following situation, taking over an existing website, I have model User which has many devices like that:
has_many :devices, :through => :credits
When I create a device it creates a credit, but some of the attributes of the credits are null. I'd like to know if there's a way to control the creation of this credit and make sure nothing is null in the credit created for the database.
Thanks in advance
Recommended:
Use default values in your credits database table. You can use a database migration to do this.
class AddDefaultValuesToCredits < ActiveRecord::Migration
def change
change_column :credits, :value1, :boolean, default: false
change_column :credits, :value2, :string, default: 'words'
# change other columns
end
end
If no explicit value is specified for value1 or value2, they'll default to false and 'words', respectively.
Alternative: You can also set default values in your Credit model.
class Credit < ActiveRecord::Base
after_initialize :set_values, unless: persisted?
# other model code
def set_values
if self.new_record?
self.value1 = false if self.value.nil? # initial boolean value
self.value2 ||= 'words' # initial string value
# other values
end
end
Related
I have this model Person
class Person
generate_public_uid generator: PublicUid::Generators::HexStringSecureRandom.new(32)
has_many :addresses, as: :resource, dependent: :destroy
accepts_nested_attributes_for :addresses, allow_destroy: true, update_only: true,
reject_if: proc { |attrs| attrs[:content].blank? }
end
in my person table, I have this public_id that is automatic generated when a person is created.
now the nested attribute in adding addresses is working fine. but the update is not the same as what nested attribute default does.
my goal is to update the addresses using public_id
class Address
generate_public_uid generator: PublicUid::Generators::HexStringSecureRandom.new(32)
belongs_to :resource, polymorphic: true
end
this is my address model
{ person: { name: 'Jack', addresses_attributes: { id: 1, content: 'new#gmail.com' } } }
this is the rails on how to update the record in the nested attribute
{ person: { name: 'Jack', addresses_attributes: { public_id: XXXXXXXX, content: 'new#gmail.com' } } }
I want to use the public_id to update records of addresses, but sadly this is not working any idea how to implement this?
Rails generally assumes that you have a single column named id that is the primary key. While it is possible to work around this, lots of tools in and around Rails assume this default – so you'll be giving yourself major headaches if you stray from this default assumption.
However, you're not forced to use integer ids. As someone else has already pointed out, you can change the type of the ID. In fact, you can supply any supported type by doing id: type, where type can e.g. be :string in your case. This should then work with most if not all of Rails' default features (including nested attributes) and also with most commonly used gems.
Since you say you are using your public_id as primary key I assume you don't mind dropping the current numbered id. The main advantage of not using an auto increment numbered key is that you don't publicly show record creation growth and order of records. Since you are using PostgreSQL, you could use a UUID is id which achieves the same goal as your current PublicUid::Generators::HexStringSecureRandom.new(32) (but does have a different format).
accepts_nested_attributes_for uses the primary key (which is normally id). By using UUIDs as data type for your id columns, Rails will automatically use those.
I've never used this functionality myself, so I'll be using this article as reference. This solution does not use the public_uid gem, so you can remove that from your Gemfile.
Assuming you start with a fresh application, your first migration should be:
bundle exec rails generate migration EnableExtensionPGCrypto
Which should contain:
def change
enable_extension 'pgcrypto'
end
To enable UUIDs for all future tables create the following initializer:
# config/initializers/generators.rb
Rails.application.config.generators do |g|
g.orm :active_record, primary_key_type: :uuid
end
With the above settings changes all created tables should use an UUID as id. Note that references to other tables should also use the UUID type, since that is the type of the primary key.
You might only want to use UUIDs for some tables. In this case you don't need the initializer and explicitly pass the primary key type on table creation.
def change
create_table :people, id: :uuid, do |t|
# explicitly set type uuid ^ if you don't use the initializer
t.string :name, null: false
t.timestamps
end
end
If you are not starting with a fresh application things are more complex. Make sure you have a database backup when experimenting with this migration. Here is an example (untested):
def up
# update the primary key of a table
rename_column :people, :id, :integer_id
add_column :people, :id, :uuid, default: "gen_random_uuid()", null: false
execute 'ALTER TABLE people DROP CONSTRAINT people_pkey'
execute 'ALTER TABLE people ADD PRIMARY KEY (id)'
# update all columns referencing the old id
rename_column :addresses, :person_id, :person_integer_id
add_reference :addresses, :people, type: :uuid, foreign_key: true, null: true # or false depending on requirements
execute <<~SQL.squish
UPDATE addresses
SET person_id = (
SELECT people.id
FROM people
WHERE people.integer_id = addresses.person_integer_id
)
SQL
# Now remove the old columns. You might want to do this in a separate
# migration to validate that all data is migrating correctly.
remove_column :addresses, :person_integer_id
remove_column :people, :integer_id
end
The above provides an example scenario, but should most likely be extended/altered to fit your scenario.
I suggest to read the full article which explains some additional info.
Because you still need an :id field in your params, unless you want to change your to_param directly in model. Try something like this:
person = Person.first
address = person.address
person.update({ name: 'Jack', adddresses_attributes: { id: address.id, public_id: XXX, _destroy: true } } )
This is the way I have my nested-attributes
#app/models/person.rb
class Person < ApplicationRecord
...
has_many :addresses, dependent: :destroy
accepts_nested_attributes_for :addresses, reject_if: :all_blank, allow_destroy: true
...
end
my controller
#app/controllers/people_controller.rb
class PeopleController < ApplicationController
...
def update
#person = Person.find_by(id: params[:id])
if #person.update(person_params)
redirect_to person_path, notice: 'Person was successfully added'
else
render :edit, notice: 'There was an error'
end
end
...
private
def person_params
params.require(:person).permit(
... # list of person fields
addresses_attributes: [
:id,
:_destroy,
... # list of address fields
]
)
end
...
end
I hope that this is able to help you.
Let me know if you need more help
My users are (booleans) students, tutors, or 'both'. My users are automatically false on all 3 unless specified, but I want the default value of 'both' to be based on a user being both a student and a tutor, therefore if student and tutor are booleans, both = student && tutor. How can I create a migration to do this?
Migration file that did not work.
class ChangeUserBoth < ActiveRecord::Migration
def change
change_column :users, :both, :boolean, :default => :student && :tutor
end
end
You can't pass a conditional method in while writing your migration code, but you can achieve the desired result through the following way:
# In your model code
before_save :set_both_based_on_student_and_tutor
private
def set_both_based_on_student_and_tutor
# Ternary way saves your form 'both' being 'nil'. It would always
# either be true or false
self.both = (self.student && self.tutor) ? true : false
end
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 a model which has some information which is best stored as a serialized Hash on the model, as it is unimportant to most of the app and varies from instance to instance:
class Foo < AR::Base
attr_accessible :name, :fields
serialize :fields
end
I have realised that one of the common entries in fields actually is relevant to the app, and would be better placed as an attribute (layout).
Bearing in mind that I should not, ideally, refer to models in migrations, how can I write a migration to add the layout field, and initialise it with the value currently in the fields Hash?
class AddLayoutToCardTemplates < ActiveRecord::Migration
def change
add_column :card_templates, :layout, :string, default: 'normal'
# Initialise `layout` from `fields['layout']`... how? With raw SQL?
end
end
You should not refer to models in your app folder. This doesn't mean you cannot create local model. :)
class AddLayoutToCardTemplates < ActiveRecord::Migration
class Foo < AR::Base
attr_accessible :name, :fields
serialize :fields
end
def change
add_column :card_templates, :layout, :string, default: 'normal'
Foo.all.each do |f|
f.layout = f.fields.delete(:layout)
f.save
end
end
That way your migration can use ActiveRecord goodies and yet stays time-independent, as your real model in app folder is never loaded.
I have an existing model in rails and I want to add AASM states to it.
From my understanding, I should add a state column to my database through migrations first and then add some states to my rails model. How do I set a default state value according to a value in another column?
Am I on the right track at all?
You are on the right track. You can set the initial state for new records in the migration itself.
Either use the :default option as follows. This is most useful if every record has the exact same starting state:
# Assuming your model is named Order
class AddStateToOrders < ActiveRecord::Migration
add_column :orders, :state, :string, :default => 'new'
end
Or you can use a simple bit of ruby to set the state of each record after the column is added. More useful if the initial state of records is conditional on something.
# Still assuming your model is named Order
class AddStateToOrders < ActiveRecord::Migration
add_column :orders, :state, :string
# Loop through all the orders, find out whether it was paid and set the state accordingly
Order.all.each do |order|
if order.paid_on.blank?
order.state = 'new'
else
order.state = 'paid'
end
order.save
end
end
Peter's answer is good, but it has one defect. You'll need to write a new migration if you change default state. So,
class AddStateToOrders < ActiveRecord::Migration
def self.up
add_column :orders, :state, :string
Order.update_all(aasm_state: 'new') # it will apply just for existing records with empty state.
end
def self.down
remove_column :orders, :state
end
end