My Classroom model has two attributes, a student_classcode and a teacher_classcode. I have an after_create callback to generate these two codes after the classroom is created. Currently, I only generate the student_classcode so far with this:
(taken from here)
class Classroom < ActiveRecord::Base
after_create :generate_token
private
MAX_RETRIES = 10
def generate_token
update_column :student_classcode, SecureRandom.hex(4)
rescue ActiveRecord::RecordNotUnique => e
#token_attempts = #token_attempts.to_i + 1
retry if #token_attempts < MAX_RETRIES
raise e, "Retries exhausted"
end
end
Right now it makes sure that the student_classcode is unique (with up to 10 retries). I want to be able to generate a teacher_classcode as well, and I want to make sure that it is unique among the teacher_classcode column and the student_classcode column.
So for example, if Classroom A has the student_classcode '12345', and (by chance) the teacher_classcode of Classroom B generates to '12345', I want Classroom B to regenerate the classcode. Or, if Classroom B has the student_classcode 'abcde', and Classroom B has the teacher_classcode 'abcde', then I want Classroom B to regenerate the teacher_classcode.
I know that the chances of there being two of the same classcode across both columns are small, but I don't want to take the chances. How can I do that?
Why don't you make Classcode a model in its own right, with validations to ensure the code is unique. Then generate a generic code and through standard belongs_to associations from the Classroom you can associate a teacher and a student classcode.
This way you can be sure each classcode is unique and only have one implementation of code generation.
Related
I have a one-to-many relationship between the classes P(parent) and C(childs).
Table C has a unique composite index {p_id, somerow}.
Having 2 objects of class P (p1 and p2), I want to combine them into one, doing this through
p2.childs.update_all (parent: p1), but I get a rollback of the transaction, because the uniqueness of the composite index is violated. However, from the point of view of internal logic, this situation is not an error, and a duplicate entry, instead of changing the parent, must be destroyed.
What is the most correct way to solve the problem?
P.S. The number of requests to the database is critical.
P.P.S. The number of children in the relation can exceed the value in 1k records.
If you are ready to validate records by Rails, which means execute a separate query to validate each record, here is a straightforward way to do it:
class C < ActiveRecord::Base
validates_uniqueness_of :somerow, scope: :p_id
belongs_to :p
end
class P < ActiveRecord::Base
has_many :childs
def merge_sibling(p2)
p2.childs.each do |c|
c.p_id = self.id
c.valid? ? c.save : c.destroy
end
p2.destroy
end
end
p1.merge_sibling p2
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!
I'm trying to create a Transaction that simultaneously is the child of a Request and also one part in a many-to-many relationship of an Inventory.
Model code:
class Transaction < ActiveRecord::Base
belongs_to :request
has_many :transactories
has_many :inventories, through: :transactories
end
class Inventory < ActiveRecord::Base
has_many :transactories
has_many :transactions, through: :transactories
end
class Transactory < ActiveRecord::Base
belongs_to :inventory
belongs_to :transaction
end
Here's the flow I'm trying to achieve:
User POSTs a request that contains additional data in a hash where key = the itemlist_id of what they want and the value = the quantity of that itemlist_id that they want. Let's say user wants two of 9, that would look like this: { 9 => 2}
For each itemlist_id in the user provided hash, I'm going to look inside the Inventories table and pull out the inventory_ids where the itemlist_id matches what the user is looking for and the owner of that inventory_id is not the user him or herself. Let's say that in the Inventories table, there are 3 ids that fulfill this: [X, Y, Z]
Now what I'd like to do is create the Transactions that belong to the Request (Request was already created earlier) and associate the Transactions and Inventories with each other. The outcome of this step is two-fold (I think it's easier to write from the perspective of what the view will look like:
that the owner of each X, Y, and Z inventory_id should see that there are 2 transactions for their item (so they can pick which one they want to honor)
that the user can see that for each of their 2 transactions, there are notifications to the owners of each X, Y, and Z
Code to create the associations
# Assume overarching parent request has been created, called #requestrecord
# Step 1, #transactionparams = { 9 => 2 }
#transactionparams.each do |itemlist_id, quantity|
# Step 2 matched_inventory_id = [X,Y,Z]
matched_inventory_id = Inventory.where.not(signup_id: #requestrecord.signup.id).where(itemlist_id: itemlist_id).ids
# Step 3, 2 transactions created each with itemlist_id of 9, each associated with inventory_ids X, Y, Z. In turn, inventory_ids X, Y, Z each associated with each of the two transactions created
quantity.to_i.times do
transaction = #requestrecord.transactions.create(itemlist_id: itemlist_id)
transaction.inventories.create matched_inventory_id
end
end
The line I can't get right is in step 3:
transaction.inventories.create matched_inventory_id
This throws an error that the parameters for create must be a hash. I also tried:
matched_inventory_id.each do |id|
transaction.inventories.create(inventory_id: id)
end
This failed because inventory_id is not a valid attribute. So... two questions:
How do I associate each of X, Y, Z inventory id with each transaction 1 and 2?
If I write one line of code to achieve above, conceivably (hopefully), I've achieved the reverse association as well? Meaning in a has_many :through, as long as I associate Inventory with Transactions, I'm automatically also associating Transactions with Inventories, right?
Finally someone answered this question here: Creating joined records using has_many :through
Basically I created the Transaction belonging to Request parent first, then associated it with the Inventories like so:
transaction.inventory_ids += matched_inventory_ids
That new line replaced this old line of code:
transaction.inventories.create matched_inventory_id
And yes, once it's associated one way, the two-way works as well since it's a many-to-many relationship.
So many tutorials on how to set up a has_many :through but not enough on how to actually do it!
I have a Inventories and Requests table joined by Bookings. Example: there could be 3 lenders who have tents in inventory, each of which is requested by 3 other borrowers. What I want to do is for each of the 3 tents in inventory, show that lender the list of 3 borrowers who requested the tent. Then the lender can pick who s/he wants to be the ultimate borrower.
I have thoughts on how this should work, but no idea if it's right, so please give advice on the below! The action is driven all by the Requests controller. Let's run through an example where the Inventories table already has 3 tents, ids [1, 2, 3]. Let's say Borrower Pat submits a Request_ID 1 for a tent.
Am I then supposed to create 3 new Bookings all with Request_ID 1 and then Inventory_ID [1, 2, 3] to get all the conceivable combinations? Something like
Inventory.where(name: "tent").each { |inventory| #request.bookings.create(inventory_id: inventory.id) }
And then is it right to use the Bookings primary key as the foreign key in both the Request and Inventory? Which means that after Borrower Pat submits his request, the bookings_id will be blank until say Lender 2 accepts, at which point bookings_id equals the id that matches the combination of Request_ID 1 and Inventory_ID 2
Now let's say when a Request is posted and a Bookings is made, I email the lender. However, I realized I don't want to bother Lender Taylor if 3 borrowers want her tent over the same time period. I'll just email her the first time, and then the subsequent ones she'll find out about when she logs in to say yes or no. In this situation is it OK to just query the Bookings table in the create action, something like (expanding off above)
-
Inventory.where(name: "tent").each do |inventory|
if !Bookings.find_by_inventory_id(inventory.id).exists?
# If there are no other bookings for this inventory, then create the booking and send an email
#request.bookings.create(inventory_id: inventory.id)
AlertMail.mail_to_lender(inventory).deliver
else
# If there are other bookings for this inventory, do any of those bookings have a request ID where the requested time overlaps with this new request's requested time? If so then just create a booking, don't bother with another email
if Bookings.where(inventory_id: inventory.id).select { |bookings_id| Bookings.find_by_id(bookings_id).request.time overlaps_with current_request.time }.count > 0
#request.bookings.create(inventory_id: inventory.id)
# If there are other bookings for this inventory but without overlapping times, go ahead and send an new email
else
#request.bookings.create(inventory_id: inventory.id)
AlertMail.mail_to_lender(inventory).deliver
end
end
end
Code above is probably flawed, I just want to know the theory of how this is supposed to be working.
Join Table
Firstly, has_many :through works by using a join table - a central table used to identify two different foreign_keys for your other tables. This is what provides the through functionality:
Some trivia for you:
has_and_belongs_to_many tables are called [plural_model_1]_[plural_model_2] and the models need to be in alphabetical order (entries_users)
has_many :through join tables can be called anything, but are typically called [alphabetical_model_1_singular]_[alphabetical_model_2_plural]
--
Models
The has_many :through models are generally constructed as such:
#app/models/inventory.rb
Class Inventory < ActiveRecord::Base
has_many :bookings
has_many :requests, through: :bookings
end
#app/models/booking.rb
Class Booking < ActiveRecord::Base
belongs_to :inventory
belongs_to :request
end
#app/models/request.rb
Class Request < ActiveRecord::Base
has_many :bookings
has_many :requests, through: :bookings
end
--
Code
Your code is really quite bloated - you'll be much better doing something like this:
#app/controllers/inventories_controller.rb
Class InventoriesController < ApplicationController
def action
#tents = Inventory.where name: "tent"
#tents.each do |tent|
booking = Booking.find_or_create_by inventory_id: tend.id
AlertMail.mail_to_lender(tent).deliver if booking.is_past_due?
end
end
end
#app/models/booking.rb
Class Booking < ActiveRecord::Base
def is_past_due?
...logic here for instance method
end
end
Used find_or_create_by
You should only be referencing things once - it's called DRY (don't repeat yourself)
I did a poor job of asking this question. What I wanted to know was how to create the actual associations once everything is set up in the DB and Model files.
If you want to create a record of B that is in a many-to-many relationship with an existing record of A, it's the same syntax of A.Bs.create. What was more important for me, was how to link an A and B that already existed, in which case the answer was A.B_ids += B_id.
Two other things:
More obvious: if you created/ linked something one way, was the other way automatic? And yes, of course. In a many-to-many relationship, if you've done say A.B_ids += B_id, you no longer have to do 'B.A_ids += A_id`.
Less obvious: if A and B are joined by table AB, the primary key of table AB doesn't need to be added to A or B. Rails wants you to worry about the AB table as less as possible, so searches, builds, etc. can all be done by A.B or B.A instead of A.AB.B or B.AB.A
I've got a Match model and a Team model.
I want to count how many goals a Team scores during the league (so I have to sum all the scores of that team, in both home_matches and away_matches).
How can I do that? What columns should I put into the matches and teams database tables?
I'd assume your Match model looks something like this:
belongs_to :home_team, class_name:"Team"
belongs_to :away_team, class_name:"Team"
attr_accessible :home_goal_count, :away_goal_count
If so, you could add a method to extract the number of goals:
def goal_count
home_matches.sum(:home_goal_count) + away_matches.sum(:away_goal_count)
end
Since this could be expensive (especially if you do it often), you might just cache this value into the team model and use an after_save hook on the Match model (and, if matches ever get deleted, then an after_destroy hook as well):
after_save :update_team_goals
def update_team_goals
home_team.update_attribute(:goal_count_cache, home_team.goal_count)
away_team.update_attribute(:goal_count_cache, away_team.goal_count)
end
Since you want to do this for leagues, you probably want to add a belongs_to :league on the Match model, a league parameter to the goal_count method (and its query), and a goal_count_cache_league column if you want to cache the value (only cache the most recently changed with my suggested implementation, but tweak as needed).
You dont put that in any table. Theres a rule for databases: Dont ever store data in your database that could be calculated from other fields.
You can calcuate that easyly using this function:
def total_goals
self.home_matches.collect(&:home_goals).inject(&:+)+self.away_matches.collect(&:away_goals).inject(&:+)
end
that should do it for you. If you want the mathes filtered for a league you can use a scope for that.