How to trigger `dependent: destroy` when updating collection_singular_ids - mysql

Consider the following setup:
class Task
has_many :users, through: :task_users
end
class User
has_many :tasks, through: :tasks_users
end
class TaskUser
# Joins table
has_many :comments, dependent: :destroy
end
class Comment
belongs_to :task_user
end
Now if I perform a standard #destroy command, such as:
tu = TaskUser.first
tu.destroy
Then all comments associated to the task-user will also be destroyed.
However, suppose you want to update a user's tasks via #collection_singular_ids=, like so:
u = User.first
puts u.task_ids # => [1, 2, 3]
u.task_ids = [1, 2]
Doing this (without even calling #save explicitly!) will trigger SQL like:
(0.3ms) BEGIN
SQL (0.4ms) DELETE FROM `task_users` WHERE `task_users`.`task_id` = 3 AND `task_users`.`user_id` = 1
(2.0ms) COMMIT
...So the associated Comments get orphaned.
The same issue occurs if you use #attributes=:
u.attributes = { task_ids: [1, 2] }
Is there a clean way to ensure that the associated Comments will always be destroyed (i.e. never orphaned)?

Thanks to #engineersmnky for pointing me in the right direction.
This may not be the prettiest solution, but a viable option is to define a callback on the association, such as:
class User
has_many :tasks,
through: :tasks_users,
before_remove: ->(user, task) do
task_user = TaskUser.find_by!(user: user, task: task)
task_user.comments.each(&:destroy!)
end
# Or alternatively, this can be defined as a method:
has_many :tasks,
through: :tasks_users,
before_remove: :destroy_task_user_comments
private
def destroy_task_user_comments(task)
task_user = TaskUser.find_by!(user: self, task: task)
task_user.comments.each(&:destroy!)
end
end
Note that I have used bang (!) methods within the block, so that the entire transaction will rollback if the an exception is raised - as per the documentation:
if any of the before_remove callbacks throw an exception, the object will not be removed from the collection.

Related

Trying to relate tables in MySQL and Rails

I'm having problems relating some tables, I have the client table with the fields ("name, age and gender") and another table called personal_documents with the fields "cpf, rg, etc ...), I tried the relationship of personal_documents belongs_to client but when i search for client only the fields of client ("name, age and gender) and "personal_documents_id" appear, the fields for personal documents ("cpf, rg, etc...) should also appear too, thanks for the help!
Code:
In client model:
has_one :personal_documents
in personal_documents model:
belongs_to :client
rails generate model Client
inside migration file you create as follow
class CreateClients < ActiveRecord::Migration[6.0]
def change
create_table :clients do |t|
t.string :user_kind
# your other field here
t.timestamps
end
end
end
rails generate model PersonalDocument
inside migration file you create as follow
class CreatePersonalDocuments < ActiveRecord::Migration[6.0]
def change
create_table :personal_documents do |t|
# this is the one that relate personal document
# to client
t.references :client, index: true
t.string :rg_front
# other field
t.timestamps
end
end
end
inside model you can declare as follow
class Client < ApplicationRecord
# please note personal_document in singular
has_one :personal_document, dependent: :destroy
accepts_nested_attributes_for :personal_document, allow_destroy: :true
# now you can do some like above for disponibility, personal_document_legal, bank_information
end
class PersonalDocument < ApplicationRecord
belongs_to :client
end
inside your controller you declare as follow
class ClientsController < ApplicationController
def barang_params
params.require(:client).permit(
:user_kind,
personal_document_attributes: [
:id,
:rg_front,
:rg_back,
:cpf,
:cnh_front,
:cnh_back,
:bank_card_front,
:address_proof,
:profile_picture
]
# this from your other question, and I think it's already correct
)
end
end
To access personal_documents of client
Client.find(1).personal_documents.cpf
To access client of personal_documents
PersonalDocument.find(id).client.name
both
document = PersonalDocument.find(id)
client = document.client
or
client = Client.find(1)
document = client.personal_documents
document.cpf
client.name
additionaly change :has_one to singular personal_document

Delete an element from a collection

I'm facing to a stupid problem. I have created a collection select which is creating elements into a join table "staffs_task" to reference an association between the model staff and task.
And now I would like two things: (1) a button delete this association (2) and a little bit of code for my model staffs_task to avoid duplication, so with the task_id and staff_id. And last info, task is a model built by ranch
my code:
(the collection in new_task)
<%= select_tag "staffs_task", options_from_collection_for_select(#staffs, 'id', 'name') , :multiple => true %>
(task_controller)
skip_before_action :configure_sign_up_params
before_action :set_ranch
before_action :set_task, except: [:create]
def create
#task = #ranch.tasks.create(task_params)
#staffs = Staff.where(:id => params[:staffs_task])
#task.staffs << #staffs
if #task.save
#task.update(done: false)
#task.update(star: false)
flash[:success] = "The task was created "
else
flash[:success] = "The task was not created "
end
redirect_to #ranch
end
private
def task_params
params.require(:task).permit(:content, :deadline, :row_order, :date, :assigned_to)
end
def set_ranch
#ranch = Ranch.find(params[:ranch_id])
end
def set_task
#task = #ranch.tasks.find(params[:id])
end
So if you have any idea about one of this two things, your help would be welcome
Thanks in advance !!
Lets say you have the following many to many setup with a join model:
class Staff
has_many :assignments
has_many :tasks, through: :assignments
end
class Task
has_many :assignments
has_many :staff, through: :assignments
end
class Assignment
belongs_to :task
belongs_to :staff
end
Note that the plural of staff is staff - unless you are talking about the sticks carried by wizards.
ActiveRecord creates "magical" _ids setters for all has_many relationships. When used with a has_many through: relationship rails is smart enough to just remove the rows from the join table.
You can use this with the collection_select and collection_checkboxes methods:
<%= form_for([#task.ranch, #task]) do |f| %>
<%= f.collection_select(:staff_ids, Staff.all, :id, :name, multiple: true) %>
<% end %>
You would then set your controller up like so:
def create
#task = #ranch.tasks.new(task_params) do |t|
# this should really be done by setting default values
# for the DB columns
t.done = false
t.star = false
end
if #task.save
redirect_to #ranch, success: "The task was created"
else
render :new, error: "The task was not created"
end
end
private
def task_params
params.require(:task)
.permit(:content, :deadline, :row_order, :date, :assigned_to, staff_ids: [])
end
staff_ids: [] will allow an array of scalar values. Also not that .new and .create are not the same thing! You where saving the record 4 times if it was valid so the user has to wait for 4 expensive write queries when one will do.

Rails searches the mistaken has_and_belongs_to_many table

I want to show all types which are related to a specific organisation in a select box of my document form. Types are part of the Ar engine. Organisations are part of another existing engine.
module Ar
module OrganisationPatch
extend ActiveSupport::Concern
included do
attr_accessible :ar_document_id
has_many :ar_documents, :class_name => 'Ar::Document'
has_and_belongs_to_many :ar_types, :class_name => 'Ar::Type'
end
end
end
module Ar
class Type < ActiveRecord::Base
attr_accessible :name
has_many :documents
has_and_belongs_to_many :organisations
end
end
class CreateTypeOrganisations < ActiveRecord::Migration
def change
create_table :ar_type_organisations, id: false do |t|
t.uuid :type_id, index: true
t.uuid :organisation_id, index: true
end
end
end
In my documents_controller I load types for forms about the before filter. The superior returns the organisation object:
def load_form_objects
unless current_user.admin?
#types = current_user.superior.ar_types
else
#types = Type.all
end
end
Calling the page I get this error and ask me why he is looking for a table called organisations_types:
ActiveRecord::StatementInvalid in Ar/documents#new
Mysql2::Error: Table 'portal.organisations_types' doesn't exist:
SELECT ar_types.* FROM ar_types INNER JOIN organisations_types
ON ar_types.id = organisations_types.type_id WHERE
organisations_types.organisation_id =
x'891c3986b33845d08d3951645a4f27d5'
Someone knows what I am doing wrong here?
Your table name isn’t map with lexical order what has_and_belongs_to_many expect. ( Expected order is organisations_types )
So you have to add :join_table option in both model's association. Like this,
has_and_belongs_to_many :ar_types, :class_name => 'Ar::Type', join_table: "ar_type_organisations"
has_and_belongs_to_many :organisations, join_table: "ar_type_organisations"
Reference

Mongoid embedded documents and Rails strong parameters not working

I have a 1-N relationship in Mongoid/Rails:
class Company
include Mongoid::Document
field :name, type: String
embeds_many :people, class_name: 'Person'
end
class Person
include Mongoid::Document
field :first_name, type: String
embedded_in :company, class_name: 'Company', inverse_of: 'people'
end
Now I can successfully create a Company as follows in the console; for example:
> c = Company.new(name: 'GLG', :people => [{first_name: 'Jake'}]) # OK!
> c.people # OK!
Then I have a JSON API controller to update a company, along the lines of:
# PUT /api/companies/:id
def update
if Company.update(company_params)
# ... render JSON
else
# ... render error
end
end
private
def company_params
params.require(:company).permit(:name, :people => [:first_name])
end
Now, when the PUT request comes in from the frontend, the company_params is always missing the :people attribute. Rails log says:
Parameters: {"id"=>"5436fbc64a616b5240050000", "name"=>"GLG", "people"=>[{"first_name"=>"Jake"}], "company"=>{"name"=>"GLG"}}
I don't get an "Unpermitted parameters" warning. I've tried every conceivable way of permitting the people field and it still doesn't get included.
params.require(:company).permit!
Results in the same. What am I doing wrong?
You have to accept nested_attributes on assignment
class Company
include Mongoid::Document
field :name, type: String
embeds_many :people, class_name: 'Person'
accepts_nested_attributes_for :people
end

Attribute mysteriously getting set to id of 1

I have an object that is apparently randomly getting set to an id of 1 and the source of where this is happening is unknown in the codebase. Could be on an update attributes for a user for which school is associated.
How can I raise an error or otherwise log when this happens so I can track it down and resolve it?
Below is a first attempt but doesn't seem to take into account update_attributes
belongs_to :foo_school, :foreign_key => 'school_id'
def foo_school=(foo_school)
begin
raise "Found" if foo_school == FooSchool.find(1)
rescue Exception => e
# Track it down through the stack trace
Rails.logger.error e
ensure
write_attribute(:foo_school, foo_school)
end
end
An observer can do this for you. What you do with this observer is up to you.
$ rails g observer Oddity
Add this in config/application.rb (search for "observer" in that file, there's an example by default).
config.active_record.observers = :oddity_observer
class OddityObserver < ActiveRecord::Observer
observe :foo
def after_update(record)
raise "Boom" if record.id == 1
end
end