About the rails way relation models - ruby-on-rails

Context:
I have two tables, challenges and challenge_steps. Both tables need to have relation between them, I need to be able to reference a Step with a Challenge and the inverse relationship.
A challenge can have multiple steps but ONLY ONE current_step.
Schema:
Challenge:
t.string "name"
t.string "subtitle"
t.text "brief", null: false
t.integer "min_team_size", default: 2, null: false
t.integer "max_team_size", default: 5, null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
Challenge::Step:
t.integer "challenge_id"
t.string "name"
t.text "description"
t.datetime "start_at"
t.datetime "end_at"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
To do this I can think of three solutions, but none of them are satisfying:
Solution One:
Challenge Model:
has_many :steps, inverse_of: :challenge, dependent: :destroy
belongs_to :current_step, class_name: Challenge::Step
Challenge::Step:
belongs_to :challenge
has_one :challenge_relation, class_name: Challenge,
foreign_key: :current_step_id, dependent: :restrict_with_error
As you can see in my Challenge::Step model I have a belongs_to(:challenge) and the Rails documentation reads:
For example, it makes more sense to say that a supplier owns an account than that an account owns a supplier.
So the behavior is OK, but the code looks odd.
Solution Two:
Create a table which contains challenge_id and step_id. Which will reference each challenge and its current_step
This one is good but it mean we need the read another table to get the needed info.
Solution Three:
add in the Challenge model:
has_many :steps, inverse_of: :challenge, dependent: :destroy do
def current_step
proxy_association.owner.steps.where(current_step: true).first
end
end
It returns a collection and the schema doesn't respect the real relation between a Challenge and his step.
What would most efficient and elegant? Could you think of a solution which would have none of these drawbacks ?

First of all, why is Challenge::Step a subclass of Challenge?
Surely you'd want it to be Step on its own? For the purposes of clarity, I will refer to it just as Step.
--
Here's what I'd do:
#app/models/challenge.rb
class Challenge < ActiveRecord::Base
has_many :steps
def current
steps.where(current: true).order(current: :desc).first
end
end
#app/models/step.rb
class Step < ActiveRecord::Base
# columns id | challenge_id | current (datetime) | etc...
belongs_to :challenge
end
This will give you the ability to call:
#challenge = Challenge.find params[:id]
# #challenge.steps = collection of steps
# #challenge.current_step = latest current step
The idea being that you could save your current_step attribute as a date in the Step model. This will have the added benefit of giving you the ability to see the historical record of when each step was "current".
--
An alternative would be to make a current column in the Challenge model:
#app/models/challenge.rb
class Challenge < ActiveRecord::Base
# columns id | name | current | etc
has_many :steps
def current_step
steps.find current
end
end
#app/models/step.rb
class Step < ActiveRecord::Base
#columns id | challenge_id | name | etc
belongs_to :challenge
end
This will allow you to call the following:
#challenge = Challenge.find params[:id]
# #challenge.steps = collection of steps
# #challenge.current_step = single instance of step
--
Your third solution is by far most elegant, but it assumes the structure you have implemented being correct.
I think you don't have the correct setup to handle the current_step attribute; you either need a way to distinguish it in the Step model or the Challenge model.

I think the first solution is 'The Rails Way' of doing what you need.
Maybe the only drawback there is the code readability, in the sense that a Challenge doesn't belong to a current step in literal terms, but I think a comment on the line should be enough, as then, the interface to access it is really meaningful: challenge.current_step

Related

Rails association on 3 tables, without "ID"?

I am learning Ruby on Rails, and I am going deeper into database models. I got stuck at the associations.
Usecase: I have a business in the port, where (small) ships come to bring in products, and then trucks to remove products. So a classical warehouse. I want to be able to have a view on my warehouse ("how many amounts of product prodref are in any of the warehouses"?).
I have three tables: Products, Warehouses and Orders. I have it working between Products and Orders and want to integrate Warehouse.
create_table "orders", force: :cascade do |t|
t.datetime "order_date"
t.integer "amount"
t.boolean "item_is_buy"
t.string "fk_prodref"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
create_table "products", force: :cascade do |t|
t.string "brand"
t.string "product_reference"
t.string "description"
t.string "category"
t.string "content_type"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["product_reference"], name: "index_products_on_product_reference", unique: true
end
create_table "warehouses", force: :cascade do |t|
t.string "wh_name"
t.string "fk_prodref"
t.integer "amount"
t.bigint "product_id", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["product_id"], name: "index_warehouses_on_product_id"
end
add_foreign_key "orders", "products", column: "fk_prodref", primary_key: "product_reference"
add_foreign_key "warehouses", "products", column: "fk_prodref", primary_key: "product_reference"
a) The field product_reference in Product is the relevant search criteria, so whenever a ship brings in, or a truck brings out, items, it is referenced by this (string) field.
b) A product can be in many warehouses (1:n association).
c) For each product going in/out, a single order is required. There is no "5 products in one order". (This would be a 1:1 association). When a ship brings 5 products, 5 orders are created.
d) each order shall now update the warehouse amount column.
I want to update Warehouse's amount column for every order in the order form. There is only one warehouse. A warehouse has no orders, and orders don't belong to warehouses. So I have no relationship between Orders and Warehouse. Continuing using only the fk_prodref, I have the fk_prodref in my order (which was captured from the product table). So my order controller (or a helper controller) could be warehouse.amount = warehouse.amount + order.amount_in, and simply fill the warehouse field fk_prodref with the fk_prodref string from the order.
I set up models Products and Orders with the foreign key on product_reference (without FK on id), and it works. I integrated Warehouse, and updates work.
Some Stack Overflow questions on 3-table associations deal with "has many : through" (a car has a motor, a motor has pistons, using car.pistons ...) but that is not the case here.
Is this a bad design, using only the foreign key, and no id related foreign keys? Am I violating Rails principles?
If I understand correctly, what you need is a many to many relationship between Products and Warehouses, not a one to many relationship. A Warehouse has many Products and a Product has many Warehouses.
In rails, this can be set up via a has_many :through association. See The Rails Guide for more details on how this works. The physicians, patients and appointments example is similar to what you are try for.
It also looks like you are missing a warehouse_id field on your Orders table. Without it, it's unclear which warehouse an order is shipping to/from given that products can be stored in many different warehouses at the same time.
To address the above, your associations should look something like the following:
class Warehouse
has_many :products, through: :inventories
has_many :orders
end
#new table required here to join Warehouses to Products
class Inventory
belongs_to :warehouse
belongs_to :product
end
class Product
has_many :orders
has_many :warehouses, through: :inventories
end
class Order
belongs_to :product
belongs_to :warehouse
end
To make the above work, you'll need to add warehouse_id and product_id fields to the new Inventories table described above and a warehouse_id field to your Orders table.
You should also remove the product_id and fk_prodref fields from your Warehouses table because you don't want a belongs_to :product association there.
In terms of other questions you asked:
It's not a violation of Rails principals to use productref instead of product_id as your foreign key. You can set up your associations using the foreign_key and primary_key options
class Order
belongs_to :product, foreign_key: :fk_productref, primary_key:
:productref
end
You can create products upfront without first creating a warehouse. The reason for the error in your current setup is that you are using a belongs_to which in recent versions of rails is required by default. If you want to make a belongs_to association optional you need to add optional: true.
thx to #cadair, I have finalized the model and added some slight extension.
I extended the models with the has_many association (to inventories):
class Product < ApplicationRecord
has_many :orders
has_many :inventories
has_many :warehouses, through: :inventories
end
class Warehouse < ApplicationRecord
has_many :orders
has_many :inventories
has_many :products, through: :inventories
end
I can use now the rails console this way:
my_product = Product.create(brand: 'svntest1', product_reference: 'svntest1', category: 'Beer')
my_warehouse = Warehouse.create(wh_name: 'svntest1')
Inventory.create product: my_product, warehouse: my_warehouse
As my product and warehouse databases have already records, I can also pick up a record, and use it in rails console this way:
my_product = Product.find_by(product_reference: 'C-Beni66')
my_warehouse = Warehouse.find_by(wh_name: 'svntest')
Inventory.create product: my_product, warehouse: my_warehouse
verifying in the postgresql database, I can then see the records in the inventories table:
# SELECT * FROM inventories;
id | product_id | warehouse_id | created_at | updated_at
----+------------+--------------+----------------------------+----------------------------
1 | 67 | 2 | 2022-12-22 22:11:40.069753 | 2022-12-22 22:11:40.069753
2 | 7 | 1 | 2022-12-22 22:17:12.287455 | 2022-12-22 22:17:12.287455
3 | 7 | 2 | 2022-12-22 22:17:22.347819 | 2022-12-22 22:17:22.347819
4 | 3 | 2 | 2022-12-23 08:16:30.508042 | 2022-12-23 08:16:30.508042
5 | 4 | 2 | 2022-12-23 08:17:58.141647 | 2022-12-23 08:17:58.141647
6 | 68 | 3 | 2022-12-23 08:34:31.618914 | 2022-12-23 08:34:31.618914
So the answer to my original question "how many amounts of product prodref are in any of the warehouses" becomes:
Warehouse.find_by(wh_name: 'svntest').products
Works like a charm :-)
One remark: I am using the model has_many through: here, which is ok, as it works. From reading the docs (again), I understand that this model is used to (also) store additional values in the join table (here: inventories). I do not store any additional info in inventories table, so I should also be able to use has_and_belongs_to_many.

Rails 5 timestamps not working for has_many through model

[ TLDR: Check your fixtures! (see answer below) ]
gem 'rails', '5.2.3'
I have two models, People and Puppies that are in a has_many "through" relationship. The join model is Companion.
class Person < ApplicationRecord
has_many :companions, -> { order(created_at: :asc) }
has_many :puppies, through: :companions
class Puppy < ApplicationRecord
has_many :companions
has_many :people, through: :companions
class Companion < ApplicationRecord
self.table_name = 'people_puppies' #is the table name messing things up?
belongs_to :person
belongs_to :puppy
default_scope { order(created_at: :asc) }
My problem is that created_at and updated_at timestamps are not working on the Companion model. When I try to assign a new relationship between two records...
#person.puppies << some_puppy
# or
#person.companions << some_puppy
# or
Companion.create!(puppy: some_puppy, person: #person)
...I get an DB constraint violation message:
ActiveRecord::NotNullViolation: SQLite3::ConstraintException: NOT NULL constraint failed: people_puppies.created_at: INSERT INTO "people_puppies" ("person_id", "puppy_id") VALUES (1052040621, 904095534)
Why isn't Rails adding the timestamps?
Here's the schema:
create_table "people_puppies", force: :cascade do |t|
t.integer "person_id", null: false
t.integer "puppy_id", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["person_id", "puppy_id"], name: "index_people_puppies_on_person_id_and_puppy_id"
t.index ["puppy_id", "person_id"], name: "index_people_puppies_on_puppy_id_and_person_id"
end
WOOPS!
The DB constraint violation wasn't actually coming from << or my test code at all. I had associations in my fixtures that were leftover from before I converted from has_and_belongs_to_many to has_many through:
Ex:
some_person:
email: foo#gmail.com
puppies:
- some_puppy
^ THIS is what was causing the DB error before my test code even started. :-/
My original question was based on incorrect assumptions, but this seems like an easy mistake to make if you refactor from HABTM to has_many through (and you have preexisting fixtures). So even though it's embarrassing, I will leave this question in case it helps someone in the future.

Relations not working as expected

This will be fairly quick and easy for most of you...I have a table called types, and another called projects. A project can only have one type, but a type can have many projects. For instance a community garden project and a playground project can both have the type of 'greenspace'. So I have set up a has_many association. In my types model I have this:
has_many :projects
and in my projects model I don't have anything (I previously had has_one in it but upon looking at the docs it seemed incorrect). In the projects#show view I would like the name of the type to display. The parks project's view should say 'greenspace'. but I am getting the error
undefined method `type' for #<Project:0x007ffdd14fcde8>
I am trying to access that name using:
<h3>Type: <%= #project.type.project_type %> </h3>
i have also tried:
<h3>Type: <%= #project.type_id.project_type %> </h3>
but of course type_id gives a number, and there is no project_type for a number. project_type being the name of the column which holds the string data 'greenspace'. Am I accessing it wrong? Or have I set it up incorrectly?
Also in my schema, projects looks like this:
create_table "projects", force: :cascade do |t|
t.string "type_id"
t.text "description"
t.integer "money_needed"
t.integer "money_raised"
t.float "interest_offered"
t.datetime "end_date"
t.integer "user_id"
t.datetime "created_at"
t.datetime "updated_at"
t.string "name"
t.text "url"
end
Project can belong_to both. Like this
#app/models/project.rb
class Project < ActiveRecord::Base
belongs_to :type
belongs_to :user
#...
end
#app/models/user.rb
class User < ActiveRecord::Base
has_many :projects
#...
end
#app/models/type.rb
class Type < ActiveRecord::Base
has_many :projects
#...
end
In the Project model you should state:
belongs_to => :type
In general, for most associations there is going to be an inverse. Not always, as you might have multiple associations in Type for Project. For example as well as your current has_many :projects, you might have others to return only projects that are unfinished, and such an association would not need an inverse.
Bear in mind that when you state: #project.type Rails is going to look for a method on #project. The association is what provides this method, and effectively the result is then the Type object that is referenced by the Project. It's important to realise that #project.type only returns a Type because the association tells it to -- the magic does not extent to just inferring that that is what is wanted.

Custom foreign_key in model gives PG::Error column does not exist - Rails

I have a VideoCollection model that will contain many records from another model (called VideoWork), using the has_many relationship. The VideoCollection model inherits from the Collection model using single table inheritance, while the VideoWork model inherits from the Work model.
I'm having a problem when I try to call up the video_works that belong to a video_collection.
In my video_collection#show action, I use the following to try to display a collection's works:
def show
#video_collection = VideoCollection.find(params[:id])
#collections = #video_collection.children
#works = #video_collection.video_works
end
But when I try to use #works in the show view, I get the following:
PG::Error: ERROR: column works.video_collection_id does not exist
SELECT "works".* FROM "works" WHERE "works"."type" IN ('VideoWork') AND "works"."video_collection_id" = $1
##(Error occurs in the line that contains <% #works.each do |work| %>)
My model files:
#----app/models/video_collection.rb----
class VideoCollection < Collection
has_many :video_works
end
#----app/models/video_work.rb----
class VideoWork < Work
belongs_to :folder, class_name: "VideoCollection", foreign_key: "folder_id"
end
The "parent" models:
#----app/models/collection.rb - (VideoCollection inherits from this)
class Collection < ActiveRecord::Base
end
#----app/models/work.rb - (VideoWork inherits from this)
class Work < ActiveRecord::Base
end
The Schema file:
#----db/schema.rb----
create_table "works", force: true do |t|
t.string "header"
t.string "description"
t.string "type"
t.string "folder_id"
end
create_table "collections", force: true do |t|
t.string "type"
t.datetime "created_at"
t.datetime "updated_at"
t.text "ancestry"
t.string "name"
t.string "tile_image_link"
end
My Question
I assume that since I have a folder_id column in the works table that I should be able to set up the belongs_to relationship properly, but it seems that Rails still wants me to have a video_collection_id column instead. I would prefer not use something specific like video_collection_id as a foreign key in the works table since I need to set up other relationships (e.g.: photo_collection has_many photo_works, etc).
What am I doing wrong here?
I don't really use has_many and belongs_to with different foreign keys than the standard, but according to the docs I would do this:
class VideoCollection < Collection
has_many :video_works, foreign_key: "folder_id"
end
class VideoWork < Work
belongs_to :folder, class_name: "VideoCollection", foreign_key: "folder_id"
end
Your Pg error says that the association is looking for 'video_collection_id' instead of 'folder_id'
Guides (chapter 4.3.2.5)

How to create a rails habtm that deletes/destroys without error?

I created a simple example as a sanity check and still can not seem to destroy an item on either side of a has_and_belongs_to_many relationship in rails.
Whenever I try to delete an object from either table, I get the dreaded NameError / "uninitialized constant" error message.
To demonstrate, I created a sample rails app with a Boy class and Dog class. I used the basic scaffold for each and created a linking table called boys_dogs. I then added a simple before_save routine to create a new 'dog' any time a boy was created and establish a relationship, just to get things setup easily.
dog.rb
class Dog < ActiveRecord::Base
has_and_belongs_to_many :Boys
end
boy.rb
class Boy < ActiveRecord::Base
has_and_belongs_to_many :Dogs
def before_save
self.Dogs.build( :name => "Rover" )
end
end
schema.rb
ActiveRecord::Schema.define(:version => 20100118034401) do
create_table "boys", :force => true do |t|
t.string "name"
t.datetime "created_at"
t.datetime "updated_at"
end
create_table "boys_dogs", :id => false, :force => true do |t|
t.integer "boy_id"
t.integer "dog_id"
t.datetime "created_at"
t.datetime "updated_at"
end
create_table "dogs", :force => true do |t|
t.string "name"
t.datetime "created_at"
t.datetime "updated_at"
end
end
I've seen lots of posts here and elsewhere about similar problems, but the solutions are normally using belongs_to and the plural/singular class names being confused. I don't think that is the case here, but I tried switching the habtm statement to use the singular name just to see if it helped (with no luck). I seem to be missing something simple here.
The actual error message is:
NameError in BoysController#destroy
uninitialized constant Boy::Dogs
The trace looks like:
/Library/Ruby/Gems/1.8/gems/activesupport-2.3.4/lib/active_support/dependencies.rb:105:in const_missing'
(eval):3:indestroy_without_callbacks'
/Library/Ruby/Gems/1.8/gems/activerecord-2.3.4/lib/active_record/callbacks.rb:337:in destroy_without_transactions'
/Library/Ruby/Gems/1.8/gems/activerecord-2.3.4/lib/active_record/transactions.rb:229:insend'
...
Thanks.
I don't see your destroy callback, but I do see a couple of problems. First, your associations need to be lowercase. So dog.rb should be:
class Dog < ActiveRecord::Base
has_and_belongs_to_many :boys
end
and boy.rb should be:
class Boy < ActiveRecord::Base
has_and_belongs_to_many :dogs
def before_save
self.dogs.build( :name => "Rover" )
end
end
Second, I believe you want to use self.dogs.create instead of self.dogs.build above, since build won't actually save the new dog object.
The accepted answer here solved my problem, only to create another one.
Here are my model objects:
class Complex < ActiveRecord::Base
set_table_name "Complexes"
set_primary_key "ComplexID"
has_and_belongs_to_many :amenities
end
class Amenity < ActiveRecord::Base
set_table_name "Amenities"
set_primary_key "AmenityID"
end
Rails uses the name of the association as the table name when creating the select query. My application runs on Unix against a legacy MySQL database and my table names are case-sensitive and don't conform to Rails conventions. Whenever my app actually tried to load the association, I would get an exception that MySQL couldn't find table amenities:
SELECT * FROM `amenities`
INNER JOIN `ComplexAmenities` ON `amenities`.AmenityID = `ComplexAmenities`.AmenityID
WHERE (`ComplexAmenities`.ComplexID = 147 )
I searched and searched and could not find a way to tell Rails to use the correct case for the table name. Out of desperation, I tried passing a :table_name option to habtm and it worked. My new Complex model looks like this:
class Complex < ActiveRecord::Base
set_table_name "Complexes"
set_primary_key "ComplexID"
has_and_belongs_to_many :amenities, :table_name => 'Amenities'
end
This works under Rails 2.3.5.
This option is not mentioned in the Ruby on Rails docs.

Resources