Rails Controller (events to activity) - ruby-on-rails

I am trying to understand, what's difference between 1 and 2 line of codes.
Is it same code ? Thank You !
Activity : has_many :events
Event : belongs_to :activity
1)
#activity = Activity.find(params[:activity_id])
event = Event.new(event_params)
event.activity_id = #activity
2) Edited, 'events' supposed tobe pluralized.
#activity = Activity.find(params[:activity_id])
event = #activity.events.new(event_params)

Yeah, in general, the two approaches are basically doing the same things and will generate same results.
In scenario 1: You are finding an activity and initializing an event, and then associating the event to the activity.
In scenario 2: You are finding an activity and then initializing one of it's associated events using events association. Although it should be: #activity.events.new(event_params) NOT #activity.event.new(event_params) [Notice events should be plural as you have a has_many association]
If you call save in both cases, you will get the same result. Basically, when you will call: activity.events you will get the list of events associated with that activity. The above-created event will be in that list in both cases.
However, although both of the scenarios are doing the same thing, the second way is considered to be more Railsy way of doing things and hence a better practice.

Two blocks are doing the same. But they are not doing the more preferred way, they are doing differently. See my comment how they are doing differently. I explained line by line.
1)
#
# Finding the activity event
#activity = Activity.find(params[:activity_id])
#
# initialising event object from events parameters
event = Event.new(event_params)
# assigning activity in event, this will help building the
# association though its a manual process. Your ORM active record
# gives the best way to handle that. Your step 2 is
# something what is preferred.
event.activity_id = #activity
#
# Comment:
# This is not the best practice. Because its not utilising Rails's
# ORM active record
2)
# finding the activity
#activity = Activity.find(params[:activity_id])
event = #activity.events.new(event_params)
# Creating event using events association
# I believe your association name is different. it should
# be plural form events.
# it should be:
event = #activity.events.new(event_params)
#
# Comment: This is the preferred way.
# Although you can do more refactoring,
# like moving the #activity on any before action
# call back to ensure it is not define every time in
# your different different action.

No they're not the same lines of code.
They tell ActiveRecord to look up particular files in specific datatables, using the appropriate foreign key:
The has_many declaration will perform a query like this:
"SELECT * FROM `events` WHERE `event`.`id` IN ?", [activity.id]
It's pinging the events data table.
--
The belongs_to will pull data out of the parent table using the provided foreign_key:
"SELECT * FROM `activities` WHERE `activity`.`event_id` IN ?", [event.id]
It's important to note that you could also use this to get a similar result:
event_id = "SELECT * FROM `activites` WHERE `activity`.`id` IN ? LIMIT 1", ["1"]
"SELECT * FROM `activities` WHERE `activity`.`event_id` IN ?", [event_id]
IE you're essentially using data from the same table, whilst has_many pulls data from another table.
Although these look similar, they are very different in the background. The has_many association denotes the possibility of extra records in another data table; the belongs_to association has to have a "parent" object.
Thus, when using has_many / belongs_to, you have to understand which is the "parent" object. For example:
#app/models/post.rb
class Post < ActiveRecord::Base
has_many :comments #-> doesn't have to be any "comment" objects
end
#app/models/comment.rb
class Comment < ActiveRecord::Base
belongs_to :post # -> only works if there is a "post" object
end
Hopefully that explains it a little clearer.
Also, you have to remember that Rails is built on top of a relational database.
This means that each time you use ActiveRecord or any of the adjoining functionality, you have to ensure that you understand what this means.
Relational databases work by taking a "foreign key" and applying it to a conjoining database. This allows your ORM (Object Relational Mapper) (in our case ActiveRecord) to pull the appropriate data from the other tables:
As such, all the associations you call within your application are basically ways to represent the above relational database setup.

Related

Rails 5: Query for record (event_type) that is not preceded by another record (event_type)

My setup looks like this:
model Order
+ has_many events
model Event
+ belongs_to :order
- type (basic attribute; no relation)
I have some event types that effectively "undo" other events. Like: complete and uncomplete. I'd like to form a query that asks the following:
"Looking at only complete and uncomplete event types, is the most recent event type complete?"
I've come up with this:
order.events.where(type: [:complete, :uncomplete]).order(created_at: :desc).first.complete?
but I'm not sure if there's a more efficient way of doing this.
Another (bonus) challenge with this
There's another model that holds several orders:
model RelatedOrder
+ has_many :orders
I'd also like to do the same query as above, but by evaluating every order in the group as complete or not.
I could use includes and include Order and Event, but then I'm guessing I should define a scope on each Order? I'm sure there's a better way than:
# model/related_orders.rb
def all_completed?
related_order.completed_orders.count == related_orders.orders.count
end
I'm using Postgres 9.5.20.

Rails attr_accessor attribute on parent model available in children

Context:
Each Order has many Items & Logistics. Each Item & Logistic (as well as the Order itself) have many Revenues.
I am creating Order + Items & Logistics at once using an accepts_nested_attributes_for on Order. However, Revenues gets created using an after_create callback on each of the models Order, Item, and Logistics. Why? Because given the difference in interpretation in these models, the code reads cleaner this way. (But if this way of doing it is what's causing this question to be asked, I will obviously reconsider!)
One key attribute that I need to store in Revenues is pp_charge_id. But pp_charge_id is not something that either Order, Items, or Logistics needs to worry about. I've attached an attr_accessor :pp_charge_id to Order, so that one works fine, however, once I'm in the child Items or Logistics models, I no longer have access to pp_charge_id which again I need to save an associated Revenue. How should I do this?
Controller Code:
#order = Order.new(params) #params includes Order params, and nested params for child Item & Logistics
#order.pp_charge_id = "cash"
#order.save #I need this to not only save the Order, the children Item & Logistics, but then to also create the associated Revenue for each of the aforementioned 3 models
ORDER Model Code:
has_many :items
has_many :revenues
attr_accessor :pp_charge_id
after_create :create_revenue
def create_revenue
self.revenues.create(pp_charge_id: self.pp_charge_id)
end
#This WORKS as expected because of the attr_accessor
ITEM/ LOGISTIC model code:
has_many :revenues
belongs_to :order
after_create :create_revenue
def create_revenue
self.revenues.create(pp_charge_id: self.order.pp_charge_id)
end
#This DOES NOT work because self.order.pp_charge_id is nil
ORDER model code:
belongs_to :order
belongs_to :item
belongs_to :logistic
Again I understand the attr_accessor is not designed to persist across a request or even if the Order itself is reloaded. But it also doesn't make sense to save it redundantly in a table that has no use for it. If the only way to do this is to put the pp_charge_id into the params for the order and save everything all at once (including Revenues), then let me know because I know how to do that. (Again, would just rather avoid that because of how it's interpreted: params are coming from User, Revenue data is something I'm providing)
I think if you want the order's pp_charge_id to apply to all its items and logistics, I'd put all that into the order's after_create callback:
# order.rb
def create_revenue
revenues.create(pp_charge_id: pp_charge_id)
items.each {|i| i.revenues.create(pp_charge_id: pp_charge_id)}
logistics.each {|l| l.revenues.create(pp_charge_id: pp_charge_id)}
end
EDIT: Alternately, you could add inverse_of to your belongs_to declarations, and then I believe Item#create_revenue would see the same Order instance that you set in the controller. So if you also added an attr_accessor to the Item class, you could write its create_revenue like this:
# item.rb
def create_revenue
revenues.create(pp_charge_id: pp_charge_id || order.pp_charge_id)
end
This should cover the new requirement you've mentioned in your comment.
instead of using after_create and accessors you should consider having a proper method that does exactly what you need, ie:
Order.create_with_charge(:cash, params)
i find it disturbing to persist redundant information in the database just because the code reads cleaner that way!

Rails preview update associations without saving to database

I want to preview what the model will look like when saved without currently saving to the database.
I am using #event.attributes = because that assigns but does not save attributes for #event to the database.
However, when I also try to assign the audiences association, Rails inserts new records into the audiences_events join table. Not cool. Is there a way to preview what these new associations will look like without inserting into the join table?
Model
class Event < ActiveRecord::Base
has_and_belongs_to_many :audiences # And vice versa for the Audience model.
end
Controller
class EventsController < ApplicationController
def preview
#event = Event.find(params[:id])
#event.attributes = event_params
end
private
def event_params
params[:event].permit(:name, :start_time, :audiences => [:id, :name]
end
end
Possible Solutions?
Possible solutions that I thought of, but don't know how to do:
Using some sort of method that assigns associations, but does not persist them.
disabling all database writes for this one action (I dont know how to do that).
Rolling back all database changes at the end of this action
Any help with these would be great!
UPDATE:
After the reading the great answers below, I ended up writing this service class that assigns the non-nested attributes to the Event model, then calls collection.build on each of the nested params. I made a little gist. Happy to receive comments/suggestions.
https://gist.github.com/jameskerr/69cedb2f30c95342f64a
In these docs you have:
When are Objects Saved?
When you assign an object to a has_and_belongs_to_many association, that object is automatically saved (in order to update the join table). If you assign multiple objects in one statement, then they are all saved.
If you want to assign an object to a has_and_belongs_to_many association without saving the object, use the collection.build method.
Here is a good answer for Rails 3 that goes over some of the same issues
Rails 3 has_and_belongs_to_many association: how to assign related objects without saving them to the database
Transactions
Creating transactions is pretty straight forward:
Event.transaction do
#event.audiences.create!
#event.audiences.first.destroy!
end
Or
#event.transaction do
#event.audiences.create!
#event.audiences.first.destroy!
end
Notice the use of the "bang" methods create! and destroy!, unlike create which returns false create! will raise an exception if it fails and cause the transaction to rollback.
You can also manually trigger a rollback anywhere in the a transaction by raising ActiveRecord::Rollback.
Build
build instantiates a new related object without saving.
event = Event.new(name: 'Party').audiences.build(name: 'Party People')
event.save # saves both event and audiences
I know that this is a pretty old question, but I found a solution that works perfectly for me and hope it could save time to someone else:
class A
has_many :bs, class_name 'B'
end
class B
belongs_to :a, class_name: 'A'
end
a.bs.target.clear
new_bs.each {|new_b| a.bs.build new_b.attributes.except('created_at', 'updated_at', 'id') }
you will avoid autosave that Rails does when you do a.bs = new_bs

Correct way to create or update with multiple belongs_to in Rails

New to Rails and Ruby and trying to do things correctly.
Here are my models. Everything works fine, but I want to do things the "right" way so to speak.
I have an import process that takes a CSV and tries to either create a new record or update an existing one.
So the process is 1.) parse csv row 2.) find or create record 3.) save record
I have this working perfectly, but the code seems like it could be improved. If ParcelType wasn't involved it would be fine, since I'm creating/retrieving a parcel FROM the Manufacturer, that foreign key is pre-populated for me. But the ParcelType isn't. Anyway to have both Type and Manufacturer pre-populated since I'm using them both in the search?
CSV row can have multiple manufacturers per row (results in 2 almost identical rows, just with diff mfr_id) so that's what the .each is about
manufacturer_id.split(";").each do |mfr_string|
mfr = Manufacturer.find_by_name(mfr_string)
# If it's a mfr we don't care about, don't put it in the db
next if mfr.nil?
# Unique parcel is defined by it's manufacturer, it's type, it's model number, and it's reference_number
parcel = mfr.parcels.of_type('FR').find_or_initialize_by_model_number_and_reference_number(attributes[:model_number], attributes[:reference_number])
parcel.assign_attributes(attributes)
# this line in particular is a bummer. if it finds a parcel and I'm updating, this line is superfulous, only necessary when it's a new parcel
parcel.parcel_type = ParcelType.find_by_code('FR')
parcel.save!
end
class Parcel < ActiveRecord::Base
belongs_to :parcel_type
belongs_to :manufacturer
def self.of_type(type)
joins(:parcel_type).where(:parcel_types => {:code => type.upcase}).readonly(false) unless type.nil?
end
end
class Manufacturer < ActiveRecord::Base
has_many :parcels
end
class ParcelType < ActiveRecord::Base
has_many :parcels
end
It sounds like the new_record? method is what you're looking for.
new_record?() public
Returns true if this object hasn’t been saved yet — that is, a record
for the object doesn’t exist yet; otherwise, returns false.
The following will only execute if the parcel object is indeed a new record:
parcel.parcel_type = ParcelType.find_by_code('FR') if parcel.new_record?
What about 'find_or_create'?
I have wanted to use this from a long time, check these links.
Usage:
http://rubyquicktips.com/post/344181578/find-or-create-an-object-in-one-command
Several attributes:
Rails find_or_create by more than one attribute?
Extra:
How can I pass multiple attributes to find_or_create_by in Rails 3?

Ruby on rails activerecord joins - select fields from multiple tables

models:
#StatusMessage model
class StatusMessage < ActiveRecord::Base
belongs_to :users
default_scope :order => "created_at DESC"
end
#User Model
class User < ActiveRecord::Base
has_many :status_messages
end
In controller I want to join these two tables and get fields from both table. for example I want email field from User and status field from StatusMessage. When I use :
#status = User.joins(:status_messages)
Or
#status = User.includes(:status_messages)
It gives me only the user table data.
How can I implement this requirement?
You need to use includes here. It preloads data so you won't have another SQL query when you do #user.status_messages.
And yes you can't really see the effect: you need to check your logs.
First of all, I don't think it is possible (and reasonable) what you want to do. The reason for that is that the relation between User and StatusMessage is 1:n, that means that each user could have 0 to n status messages. How should these multitudes of attributes be included in your user model?
I think that the method joints in class ActiceRecord has a different meaning, it is part of the query interface. See the question LEFT OUTER joins in Rails 3
There are similar questions on the net, here is what I have found that matches most:
Ruby on Rails: How to join two tables: Includes (translated for your example) in the user a primary_status_message, which is then materialized in the query for the user. But it is held in one attribute, and to access the attributes of the status_message, you have to do something like that: #user.primary_status_message.status
When you use #status = User.includes(:status_messages) then rails eagerley loads the data of all the tables.
My point is when you use this User.includes(:status_messages) it will loads the data of status_messages also but shows only users table data then if you want first user status_messages then you have to #user.first.status_messages

Resources