Rails association on 3 tables, without "ID"? - ruby-on-rails

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.

Related

Ruby on Rails Foreign Keys Issue

I am trying to get a handle on how to use foreign keys in Rails,
$ rails g scaffold categories cat:string value:integer
$ rails db:migrate
Then create a new table with a foreign key connecting to the first table categories,
$ rails g scaffold subcategories subcats:string subcatsvalue:integer categories:references
$ rails db:migrate
Then I append /categories to the url and the form is there as expected and I can do all CRUD operations.
Then I append /subcategories to the url and try to add some data to the form such as,
Subcats: blah
Subcatsvalue: 123
Categories: cat1
should this be the id of the category or the name of the category?
/RubyLearningApp/db/migrate/20200413195730_create_categories.rb
class CreateCategories < ActiveRecord::Migration[5.0]
def change
create_table :categories do |t|
t.string :cat
t.integer :value
t.timestamps
end
end
end
/RubyLearningApp/db/migrate/20200413200303_create_subcategories.rb
class CreateSubcategories < ActiveRecord::Migration[5.0]
def change
create_table :subcategories do |t|
t.string :subcats
t.integer :subcatsvalue
t.references :categories, foreign_key: true
t.timestamps
end
end
end
Is this correct way to set up a foreign key between tables?
When I fill in the Categories with 'cat1' I get the following error,
Schema.rb
ActiveRecord::Schema.define(version: 20200413200303) do
create_table "categories", force: :cascade do |t|
t.string "cat"
t.integer "value"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
create_table "subcategories", force: :cascade do |t|
t.string "subcats"
t.integer "subcatsvalue"
t.integer "categories_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["categories_id"], name: "index_subcategories_on_categories_id"
end
end
The model files:
category.rb
class Category < ApplicationRecord
end
subcategory.rb
class Subcategory < ApplicationRecord
belongs_to :categories
end
Any help would be greatly appreciated,
Thanks,
There are a few things wrong with your scaffolds that may be causing the problems. The correct way to generate a scaffold is to use a singular scaffold name:
rails g scaffold Category
and
rails g scaffold SubCategory
This will use Rails built in Inflector to pluralize the names where necessary.
When using references, you should also use the singular:
category:references
This is the Rails Way and it will sort out most of the problems you are having. The other issue is if you want to add the category to the url, you should nest your routes:
resources :categories do
resources :sub_categories
end
This will allow you to use routes like
http://localhost:3000/categories/1/subcategories
and
http://localhost:3000/categories/1/subcategories/1
The first number (the one closest to the left) is the category id and can be access by using params[:category_id] in the sub_categories_controller.rb file. The second number (the one closest to the right) is the sub_category id and can be accessed by params[:id] in the sub_categories_controller.rb file.
Well, after spending two days stuck figuring out how to solve the foreign key issue in Rails 6+ - even though i read a lot of comments from S.O which did not do much help. I finally found the solution.
Using add_reference in your migration, you can easily solve this.
Let's pick it up from where you have model files untouched and Rails generated.
For your Category Model, you should have:
class Category < ApplicationRecord
has_many :subcategories, foreign_key: :categories_id
end
And for your Subcategory Model, you should have:
class SucCategory < ApplicationRecord
belongs_to :category. foreign_key: :categories_id
end
This creates an Association atrribute that tells rails that a Category has many Subcategories that can be identified in the categories table by a foreign key found in the subcategories table known as categories_id
Then in your console, now run the command rails generate migration AddSubcategoriesToCategories to create a migration file. Within the generated migration file, be sure to have the change method;
class AddSubcategoriesToCategories < ActiveRecord::Migration[6.0]
def change
add_references :categories, :categories, references: :subcategories, type: :integer, index: false
end
end
This would create a categories_id column in your categories table and tells ActiveRecord to reference the values(s) from the subcategories table, automatically making it a foreign key.
Funny enough, the reason why the option :categories appears a second time is because ActiveRecord by default, looks for the column named id within the table from which the foreign key is taken - as it is the default index on creating tables. But as a different column with a different name is defined as the index, you will have to specify the name of the column (eg. keyname) in the add_reference function to make ActiveRecord append the phrase _id to what you just defined as the column name and find that column - now named 'keyname_id', else you'll receive errors that specify that the column 'id' referenced in foreign key constraint does not exist or if you specify the full column name as 'keyname_id' in your add_reference function, you'll receive errors that specify that the column 'keyname_id' referenced in foreign key constraint does not exit
So in this case the second :categories in the function is the first part of the name of the column to which ActiveRecord appends the remaining part '_id' to become :categories_id.
Drawback: All your foreign keys would then have to be snakecased as 'whateverkeyname_id' in your tables

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.

How to keep attributes from two different tables synchronized in RoR?

What I want is to be able to easily be able to find the team name associated with a membership without having to send another request to the database. My plan was to just add a team_name attribute to the Memberships table, but I can't think of a way to have this new attribute stay in sync with the name attribute in the Teams table. In other words, I want it to be the case that, if the owner of a team changes the team's name, then all memberships to that team will be updated (with the new team name) as well.
Here is what my setup looks like. I've fairly new to Rails.
/app/models/membership.rb
class Membership < ActiveRecord::Base
belongs_to :user
belongs_to :team
end
/app/models/team.rb
class Membership < ActiveRecord::Base
belongs_to :user
belongs_to :team
end
/app/db/schema.rb
ActiveRecord::Schema.define(version: 20161022002620) do
create_table "memberships", force: :cascade do |t|
t.integer "user_id"
t.integer "team_id"
t.datetime "created_at"
t.datetime "updated_at"
end
create_table "teams", force: :cascade do |t|
t.string "name"
t.integer "user_id"
end
end
If there is a better way to achieve what I am asking, then please let me know as well.
With this relational data your membership doesn't need a team name attribute - it is already available through the team association.
Generally there's no reason to keep data 'in sync' in this way unless you're performing some sort of computation. You don't need to store a name attribute on Membership - you can just use the existing one in Team.
I have seen people add duplicate database columns because they don't know how to traverse through associations. But unless you're using some noSql system, this isn't the 'right way' to do it - there is an underlying SQL API (through ActiveRecord) that performs lookups very efficiently.
in response to your comment. Do this:
class Membership < ActiveRecord::Base
def name
team.name
end
end

About the rails way relation models

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

Table Relationship Reference Ruby on Rails

I am currently working on a web application where a contractor (electrician, roofer, plumber) etc can make a proposal online. Pictures, youtube videos of the project, and a text description will be provided to the contractor from the customer.
So far I am working on the pictures feature using carrierwave
This is the table of this model in my schema
create_table "project_pictures", force: true do |t|
t.string "name"
t.string "picture"
t.datetime "created_at"
t.datetime "updated_at"
end
Here are my two records in my rails console
ProjectPicture Load (0.4ms) SELECT "project_pictures".* FROM "project_pictures"
=> #<ActiveRecord::Relation [#<ProjectPicture id: 2, name: "Request for Siding Quote Customer A", picture: "siding.jpg", created_at: "2013-08-15 16:10:22", updated_at: "2013-08-15 16:47:02">, #<ProjectPicture id: 1, name: "Request for Siding Quote Customer A", picture: "sidingrequest.jpg", created_at: "2013-08-14 01:54:27", updated_at: "2013-08-15 16:47:39">]>
The thing is I am trying to link multiple pictures to one customer. Lets say the above two pictures belong to one customer and there are two rows because there are two pictures.
How do I reference that in the table, lets say I have one customer and thats me "judy"
both the record should reference judy's id?
and then eventually in the view, I can draw both pictures out using an image tag that belong to the customer id 1 - with name = "judy" or just customer id = 1?
If I am not making things clear please let me know, I am not that familiar with tables relationships and which relationship will help me the most.
create table :projects do |t|
t.references :clients
t.timestamps
end
create_table :pictures do |t|
t.string :name, null:false
t.string :location, null:false
t.references :projects, null:false
t.timestamps
end
... meanwhile.. back in the model layer
class Project < ActiveRecord::Model
has_many :pictures, :dependent => destroy
end
class Picture < ActiveRecord::Model
belongs_to :project
validates_presence_of :project
end
To get all Judy's pictures
Client.where("name like ?", "Judy").projects.first.pictures
I would have set it up like this
user has_many project_pictures
project_pictures belongs_to user
your project picture table should have a row for the user_id.

Resources