Rails 4 & ActiveRecord: prevent dependent destroy if owner exists - ruby-on-rails

Given the following classes:
class User < ActiveRecord::Base
has_one :profile, dependent: :destroy
end
class Profile < ActiveRecord::Base
belongs_to :user
end
How do I prevent ActiveRecord from destroying a profile which owner user exists? I mean, it should not be possible to destroy a profile if there's a user who owns it.
I did in this way:
class User < ActiveRecord::Base
has_one :profile
after_destroy :destroy_profile
private
def destroy_profile
profile.destroy
end
end
class Profile < ActiveRecord::Base
belongs_to :user
before_destroy :check_owner_user
def check_owner_user
unless user.nil?
raise CustomException.new("Cannot delete while its owner user exists.")
end
end
end
It seems to me an over worked solution. Does Rails or ActiveRecord provide a better and more concise solution?

Related

【Rails】acts_as_paranoid environment, I want to physically delete records that use the relation dependent: :destroy

detail
user.rb
class User < ApplicationRecord
acts_as_paranoid
has_many :posts, dependent: :destroy
post.rb
class Post < ApplicationRecord
acts_as_paranoid
belongs_to :user
users_controller.rb
def destroy
user = User.find(params[:id])
user.destroy!
end
When a user is deleted, how should I describe that the user is logically deleted and the post related to the deleted user is physically deleted?
I would like to ask for your wisdom.
environment
rails 6.0
How about doing it in the after_destroy callback?
Looking at the docs I don't see a paranoid equivalent of destroy_all! so you're going to have to do a loop and fall destroy_fully! (which is what paranoia does so this will be no slower src)
class User < ApplicationRecord
acts_as_paranoid
has_many :posts
after_destroy :delete_deps
def delete_deps
self.posts.each do |post|
post.destroy_fully!
end
end
end

dependent: :destroy doesn't work on has_one relation

If I delete child record so parent record does not get deleted automatically.
class User < ActiveRecord::Base
has_one :agency, dependent: :destroy
accepts_nested_attributes_for :agency
end
class Agency < ActiveRecord::Base
belongs_to :user
accepts_nested_attributes_for :user
end
if #agency.present?
#agency.user.destroy
flash[:notice] = 'Agency Deleted'
end
Destroy child record so parent record automatically destroy.
I think, your models could be re-written like this to achieve expected output.
class User < ActiveRecord::Base
has_one :agency # Change
accepts_nested_attributes_for :agency
end
class Agency < ActiveRecord::Base
belongs_to :user, dependent: :destroy # Change
accepts_nested_attributes_for :user
end
if #agency.present?
#agency.destroy # Change
flash[:notice] = 'Agency Deleted'
end
Let's think logically now.
What have you changed is, you made User dependent on Agency and now it's rails doable to form a parent-child relationship to get accepted output. So when you destroy an #agency, it will also delete the dependent user record.
You should use the following code to delete a user and its associated agency without making any change to your model.
class User < ActiveRecord::Base
has_one :agency, dependent: :destroy
accepts_nested_attributes_for :agency
end
class Agency < ActiveRecord::Base
belongs_to :user
accepts_nested_attributes_for :user
end
if #agency.present?
user = #agency.user #Change
user.destroy # This will destroy both user and associated agency.
flash[:notice] = 'Agency and User Deleted'
end
A complete official guide on dependent: :destroy can be find here.

How can I implement a has_many :through association two ways?

I have a property model and a user model.
A user with the role of 'admin', which is represented by a column on the users table, can have many properties.
A user with a role of 'guest' can also belong to a property, which gives them access to that property.
How should I do this in Rails?
authorizations table -> user_id, property_id
class Authorization < ActiveRecord::Base
belongs_to :user
belongs_to :property
end
class User < ActiveRecord::Base
has_many :authorizations
has_many :properties, through: :authorizations
end
class Property < ActiveRecord::Base
has_many :authorizations
has_many :users, through: :authorizations
end
then you can do User.find(id).properties
First, you need a has_many :through association between your models User and Property. So, create a new table properties_users with columns user_id and propety_id. And do following changes to the models:
class PropertiesUser < ActiveRecord::Base
belongs_to :user
belongs_to :property
end
class User < ActiveRecord::Base
has_many :properties_users
has_many :properties, through: :properties_users
end
class Property < ActiveRecord::Base
has_many :properties_users
has_many :users, through: :properties_users
end
Now, we need to make sure that a guest user does not have more than one property. For that we can add a validation to model PropertiesUser like below:
class PropertiesUser < ActiveRecord::Base
validate :validate_property_count_for_guest
private
def validate_property_count_for_guest
return unless user && user.guest?
if user.properties.not(id: self.id).count >= 1
self.errors.add(:base, 'guest user cannot have more than one properties')
end
end
end
class User < ActiveRecord::Base
def guest?
# return `true` if user is guest
end
end
Finally, to access a guest user's property, define a dedicated method in model User:
class User < ActiveRecord::Base
def property
# Raise error if `property` is called on non-guest users
raise 'user has multiple properties' unless guest?
properties.first
end
end
Now, you can fetch a guest user's property by running:
user = User.first
user.guest?
=> true
user.property
=> <#Property 1> # A record of Property

ActiveRecord polymorphic association with unique constraint

I have a site that allows users to log in via multiple services (LinkedIn, Email, Twitter, etc..).
I have the below structure set up to model a User and their multiple identities. Basically a user can have multiple identieis, but only one of a given type (e.g. can't have 2 Twitter identiteis).
I decided to set it up as a polymorphic relationship, as drawn below. Basically there's a middle table identities that maps a User entry to multiple *_identity tables.
The associations are as follows (shown only for LinkedInIdentity, but can be extrapolated)
# /app/models/user.rb
class User < ActiveRecord::Base
has_many :identities
has_one :linkedin_identity, through: :identity, source: :identity, source_type: "LinkedinIdentity"
...
end
# /app/models/identity
class Identity < ActiveRecord::Base
belongs_to :user
belongs_to :identity, polymorphic: true
...
end
# /app/models/linkedin_identity.rb
class LinkedinIdentity < ActiveRecord::Base
has_one :identity, as: :identity
has_one :user, through: :identity
...
end
The problem I'm running into is with the User model. Since it can have multiple identities, I use has_many :identities. However, for a given identity type (e.g. LinkedIn), I used has_one :linkedin_identity ....
The problem is that the has_one statement is through: :identity, and there's no singular association called :identity. There's only a plural :identities
> User.first.linkedin_identity
ActiveRecord::HasManyThroughAssociationNotFoundError: Could not find the association :identity in model User
Any way around this?
I would do it like so - i've changed the relationship name between Identity and the others to external_identity, since saying identity.identity is just confusing, especially when you don't get an Identity record back. I'd also put a uniqueness validation on Identity, which will prevent the creation of a second identity of the same type for any user.
class User < ActiveRecord::Base
has_many :identities
has_one :linkedin_identity, through: :identity, source: :identity, source_type: "LinkedinIdentity"
end
# /app/models/identity
class Identity < ActiveRecord::Base
#fields: user_id, external_identity_id
belongs_to :user
belongs_to :external_identity, polymorphic: true
validates_uniqueness_of :external_identity_type, :scope => :user_id
...
end
# /app/models/linkedin_identity.rb
class LinkedinIdentity < ActiveRecord::Base
# Force the table name to be singular
self.table_name = "linkedin_identity"
has_one :identity
has_one :user, through: :identity
...
end
EDIT - rather than make the association for linkedin_identity, you could always just have a getter and setter method.
#User
def linkedin_identity
(identity = self.identities.where(external_identity_type: "LinkedinIdentity").includes(:external_identity)) && identity.external_identity
end
def linkedin_identity_id
(li = self.linkedin_identity) && li.id
end
def linkedin_identity=(linkedin_identity)
self.identities.build(external_identity: linkedin_identity)
end
def linkedin_identity_id=(li_id)
self.identities.build(external_identity_id: li_id)
end
EDIT2 - refactored the above to be more form-friendly: you can use the linkedin_identity_id= method as a "virtual attribute", eg if you have a form field like "user[linkedin_identity_id]", with the id of a LinkedinIdentity, you can then do #user.update_attributes(params[:user]) in the controller in the usual way.
Here is an idea that has worked wonderfully over here for such as case. (My case is a tad diffferent since all identites are in the same table, subclasses of the same base type).
class EmailIdentity < ActiveRecord::Base
def self.unique_for_user
false
end
def self.to_relation
'emails'
end
end
class LinkedinIdentity < ActiveRecord::Base
def self.unique_for_user
true
end
def self.to_relation
'linkedin'
end
end
class User < ActiveRecord::Base
has_many :identities do
[LinkedinIdentity EmailIdentity].each do |klass|
define_method klass.to_relation do
res = proxy_association.select{ |identity| identity.is_a? klass }
res = res.first if klass.unique_for_user
res
end
end
end
end
You can then
#user.identities.emails
#user.identities.linkedin

RAILS: How to make new collection.build in a callback?

How to create a new record in after_save using other model?
I tried this line which resulted "undefined method `journals' for nil:NilClass"
e.g.
resources :users do
resource :profile
resources :journals
end
class User < ActiveRecord::Base
has_one :profile
has_many :journals
end
class Profile < ActiveRecord::Base
belongs_to :user
after_save :create_new_journal_if_none
private
def create_new_journal_if_none
if user.journals.empty? ????
user.journals.build() ????
end
end
end
class Journals < ActiveRecord::Base
belong_to :user
end
Nested models are going to be saved as well once parent saves, so it's easy to use before_create block and build a nested resource here.
class Profile < ActiveRecord::Base
belongs_to :user
before_create do
user.journals.build unless user.journals.any?
end
end
This line of code will create a profile and a journal assigned with the User
User.find(1).create_profile(name :test)

Resources