Store Join Model Data in Rails [duplicate] - mysql

This question already has answers here:
Rails HABTM setup, model object, & join_table insertion controller setup
(2 answers)
Closed 2 years ago.
I'm new to Ruby on Rails, and I'm developing a backend API.
Currently, I got 2 Active Record Models called Book and Genre.
Active Record Model
class Book < ActiveRecord::Base
has_and_belongs_to_many :genres
end
class Genre < ActiveRecord::Base
hast_and_belongs_to_many :books
end
DB Schema Model
create_table :books do |t|
t.string "title"
end
create_table :genres do |t|
t.string "genre"
end
create_join_table :books, :genres do |t|
t.index [:book_id, :genre_id]
t.index [:genre_id, :book_id]
end
REST POST Request
# POST /book
def create
book= Book.new(books_params)
if book.save
render json: {status: 'SUCCESS', message:'Book Saved', data: book},status: :ok
else
render json: {status: 'ERROR', message: 'Booknot saved', data: book.errors}, status: :unprocessable_entity
end
end
private
def books_params
params.require(:book).permit(:title)
end
QUESTION
I'd like to make an HTTP Post request to create a new book with it's genres. I've already tested the book insertion (without genres, just passing the book name), it works perfectly. However I'd also like to add some genre categories.

Both the has_many and has_and_belongs_to_many class methods create a set of _ids setters and getters:
book = Book.new
book.genre_ids = [1, 2, 3]
book.genre_ids == [1, 2, 3] # true
These setters will automatically create and delete rows in the join table. Since ActiveModel::AttributeAssignment maps the hash argument to .new, .update and .create to setters in the model all you need to do is whitelist the genre_ids parameter:
def books_params
params.require(:book).permit(:title, genre_ids: [])
end
Passing an empty array permits an array of permitted scalar values like for example numericals or strings.

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

Is there a way to preload an arbitrary number of parent associations in Rails?

TL;DR: I have a model that belongs_to :group, where group is another instance of the same model. That "parent" group can also have a parent, and so on up the chain. Is there a way to includes this structure as far up as it goes?
I have a Location model, which looks like this (abridged version):
create_table "locations", force: :cascade do |t|
t.string "name"
t.decimal "lat", precision: 20, scale: 15
t.decimal "long", precision: 20, scale: 15
t.bigint "group_id"
t.string "type"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["group_id"], name: "index_locations_on_group_id"
end
class Location < ApplicationRecord
belongs_to :group, class_name: 'Location', required: false
has_many :locations, foreign_key: 'group_id', dependent: :destroy
end
In other words, it can optionally belong to a "parent" instance of itself, referenced as group.
That parent instance can also belong to a parent instance another level up, as can its parent, etc etc. Elephants, all the way down.
What I'd like to do is string the names of a Location and all its parent instances together, so I end up with something like "Top-level group > Mid-level group > Lowest group > Location". This is fine, and I've implemented that in the model already:
def parent_chain
Enumerator.new do |enum|
parent_group = group
while parent_group != nil
enum.yield parent_group
parent_group = parent_group.group
end
end
end
def name_chain
(parent_chain.map(&:name).reverse + [name]).join(" \u00BB ")
end
The only problem with this, however, is that it will query individually for each parent instance as it gets there (the N+1 problem). Once I'm including several Locations in a single page, this is a lot of queries slowing the load down. I'd like to preload (via includes) this structure as I would for a normal belongs_to association, but I don't know if there's a way to include an arbitrary number of parents like this.
Is there? How do I do it?
Using includes? No. Recursive preloading could be achieved this way though:
Solution #1: True recursion
class Location
belongs_to :group
# Impure method that preloads the :group association on an array of group instances.
def self.preload_group(records)
preloader = ActiveRecord::Associations::Preloader.new
preloader.preload(records, :group)
end
# Impure method that recursively preloads the :group association
# until there are no more parents.
# Will trigger an infinite loop if the hierarchy has cycles.
def self.deep_preload_group(records)
return if records.empty?
preload_group(records)
deep_preload_group(records.select(&:group).map(&:group))
end
end
locations = Location.all
Location.deep_preload_group(locations)
The number of queries will be the depth of the group hierarchy.
Solution #2: Accepting a hierarchy depth limit
class Location
# depth needs to be greather than 1
def self.deep_preload_group(records, depth=10)
to_preload = :group
(depth - 1).times { to_preload = {group: to_preload} }
preloader = ActiveRecord::Associations::Preloader.new
preloader.preload(records, to_preload)
end
end
The number of queries will be the minimum of depth and the actual depth of the hierarchy

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.

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

How to save data directly from the migration file in ruby on rails

In my ROR project, I came across an issue. Previous developer of the project has save an array of data into a single column for a particular field. But now I have to get those data and save it to another separate table with the current table id. Following is my code for get the details from the db:
#ar = AResponse.select("id, selected_barriers");
#ar.each do |p|
p.selected_barriers.each do |barrier|
end
end
What I wanted is I have to save the p.id and barrier to a new table and I have to do it in a migration file. So when the migration runs, it will get all the existing details from AResponse and save it to the new one. Can I do it only with migration file? If so how can I do it?
This is my full code
I tried but its not working: Following is my code
class AnalysisBarriers < ActiveRecord::Migration
def self.up
end
def change
create_table :analysis_barriers do |t|
t.integer :analysis_response_id, :null => false
t.string :barrier
end
#analysis_response = AnalysisResponse.select("id, selected_barriers");
#analysis_response.each do |p|
p.selected_barriers.each do |barrier|
AnalysisBarriers.create(:analysis_response_id => p.id, :barrier => barrier)
end
end
end
def self.down
drop_table :analysis_barriers
end
end
Thanks
Yes you can do data changes after migrations. Whatever data changes you want to do, you can do in change function or up function. Take the following example
class Event < ActiveRecord::Migration
def change
create_table events do |t|
t.datetime :starts_at
t.datetime :ends_at
t.timestamps
end
Event.create(:starts_at => Time.now, :ends_at => Time.now+1)
end
end
Above statement will add data to your table just after its creation.