I added a has_and_belongs_to_many between Product and Brand tables/models
This is how the models look like:
class Brand < ActiveRecord::Base
has_and_belongs_to_many :products
default_scope { order('name asc')}
end
class Product < ActiveRecord::Base
has_and_belongs_to_many :brands
end
These are the existing columns in the table:
[13] pry(main)> Brand
=> Brand(id: integer, name: string, created_at: datetime, updated_at: datetime, product_id: integer)
[11] pry(main)> Product
=> Product(id: integer, name: string, created_at: datetime, updated_at: datetime)
Join table db migration:
class CreateJoinTableProductsBrands < ActiveRecord::Migration
def change
create_join_table :products, :brands do |t|
t.integer :product_id
t.integer :brand_id
t.index [:product_id, :brand_id]
t.index [:brand_id, :product_id]
end
end
end
Questions:
As you will notice, the Brand table already had the product_id column. Should I change it to an array product_ids column? (I am using postgres)
Should I add brand_ids column to Product
Should I add ProductBrand model. I tried it but seems like Rails console didnt recognize it
class ProductBrand < ActiveRecord::Base
end
In ActiveAdmin what's the correct way of creating a new entry for Product or Brand such that a new record correctly links Product, Brand and ProductBrand entry?
I would highly recommend reading some tutorials and documentation that explain the fundamental difference between HATBM or HMT relationships in ActiveRecord. I think this would go a long way to answering your own questions.
Take the following tables below: Teams and Users (which could be equivalent to your brands and products). They likewise have a HATBM relationship both ways between them.
create_table "teams", force: :cascade do |t|
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.integer "wins"
t.float "win_percentage"
end
create_table "teams_users", id: false, force: :cascade do |t|
t.integer "team_id", null: false
t.integer "user_id", null: false
end
add_index "teams_users", ["team_id", "user_id"], name: "index_teams_users_on_team_id_and_user_id", using: :btree
add_index "teams_users", ["user_id", "team_id"], name: "index_teams_users_on_user_id_and_team_id", using: :btree
create_table "users", force: :cascade do |t|
t.string "first_name"
t.string "last_name"
t.string "user_name"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
Note that there is no foreign key (FK) of a user_id in the teams table, and no FK in the users table. The purpose of the join table is to join these 2 models hence no FK is needed in each to link them.
To answer your first 3 questions:
I would not change your product_ids column to an array. There is simply no need. This is the purpose of the join table.
I would not add a brand_ids column to product. Use your join table instead.
Unless you have a specific reason for requiring a ProductBrand model then you do not need it. If did require it then I would advocate the use of have_many through relationships. Follow the Rails conventions and if using ActiveRecord's HATBM association you don't need/want this.
This question/answer will help:
has_and_belongs_to_many vs has_many through
and an excellent post explaining join tables:
http://nishacodes.tumblr.com/post/73484141822/join-tables-heres-the-deal
Related
I'm trying to save data fetched from Sellix API into the db in my Rails application.
Basically, there are 4 models: products, coupons, orders, and feedback.
Sellix has its own unique id on every object called "uniqid" so I decided to use it as the primary key in my models as well.
For some models, I want to save references for other tables. For example, I want to have a coupon as a reference for orders to find out which coupon has been used when placing that order.
This is how two schemas are now:
create_table "coupons", id: false, force: :cascade do |t|
t.string "uniqid", null: false
t.string "code"
t.decimal "discount"
t.integer "used"
t.datetime "expire_at"
t.integer "created_at"
t.integer "updated_at"
t.integer "max_uses"
t.index ["uniqid"], name: "index_coupons_on_uniqid", unique: true
end
create_table "orders", id: false, force: :cascade do |t|
t.string "uniqid", null: false
t.string "order_type"
t.decimal "total"
t.decimal "crypto_exchange_rate"
t.string "customer_email"
t.string "gateway"
t.decimal "crypto_amount"
t.decimal "crypto_received"
t.string "country"
t.decimal "discount"
t.integer "created_at"
t.integer "updated_at"
t.string "coupon_uniqid"
t.index ["uniqid"], name: "index_orders_on_uniqid", unique: true
end
The coupon_uniqid on orders table is the reference to the relevant coupon.
The order object on Sellix API already has that reference so currently I can save it this way.
But when I display all orders, I have to use Coupon.find_by(uniqid: order.coupon_uniqid) and it always iterate through every coupon record in the local db to find it as below.
CACHE Coupon Load (0.0ms) SELECT "coupons".* FROM "coupons" WHERE "coupons"."uniqid" = $1 LIMIT $2 [["uniqid", "62e95dea17de385"], ["LIMIT", 1]]
I can get rid of that if I can keep the coupon reference instead of the uniqid.
That's basically what I want to figure out.
YAGNI. A better approach that you should consider is to just have your own primary key and treat the uniqid as a secondary identifier to be used when looking up records based on their external id.
That way everything just works with minimal configuration.
If you really want to break the conventions you can configure the type and name of the primary key when creating the table:
create_table :orders, id: :string, primary_key: :uniqid do |t|
# ...
end
class Order < ApplicationRecord
self.primary_key = :uniqid
end
Since this column won't automatically generate primary keys you'll need to deal with that in all your tests as well.
You then have to provide extra configuration when creating foreign key columns so that they are the same type and point to the right column on the other table:
class AddOrderIdToCoupons < ActiveRecord::Migration[7.0]
def change
add_reference :coupons, :order,
type: :string, # default is bigint
null: false,
foreign_key: { primary_key: "uniqid" }
end
end
And you also need to add configuration to all your assocations:
class Coupon < Application
belongs_to :order, primary_key: "uniqid"
end
class Order < Application
has_many :coupons, primary_key: "uniqid"
end
I am beginner in Ruby-on-Rails and I started to investigate ActiveRecords. And I run into one problem.
I have two tables: todo_lists and users. The first one has following records: title, description, user_id, deadline. And the second one has only one record: name. I want to get a table that contains following records: title, description, name, deadline, i.e. combine two tables and put user instead of user_id. I try to solve this problem using joins:
TodoList.joins(:user).select("todo_lists.title, todo_lists.description, users.name, todo_lists.deadline")
But I get the new ActiveRecord::Relation without users.name:
[#<TodoList id: nil, title: "This is title", description: "This is description", deadline: "2020-03-13 18:59:58">]
This is my todo_list.rb and user.rb files:
class TodoList < ApplicationRecord
belongs_to :user
end
class User < ApplicationRecord
has_many :todo_lists
end
schema.rb file:
ActiveRecord::Schema.define(version: 2020_03_13_183504) do
create_table "todo_lists", force: :cascade do |t|
t.string "description", null: false
t.integer "user_id", null: false
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
t.boolean "is_disabled"
t.datetime "deadline"
end
create_table "users", force: :cascade do |t|
t.string "name", null: false
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
end
add_foreign_key "todo_lists", "users"
end
Can anyone help me to solve this problem?
If what you need is to access the user name from each object in the TodoList::ActiveRecord_Relation, you can use AS and give the proper name, which allows you to use it as a method from a single record on the ActiveRecord_Relation holding the user name:
TodoList.joins(:user).select("..., users.name AS user_name").first.user_name
Otherwise you can use as_json, which returns an array containing the data of every record as a hash, where each key is the column and the value, the corresponding values in the select statement:
TodoList.joins(:user).select("todo_lists.description, users.name").as_json
# [{"id"=>nil, "description"=>"1st", "name"=>"user1", ...},
# {"id"=>nil, "description"=>"2nd", "name"=>"user2", ...}]
Sebastian Palma's is a good answer to the question as asked. It asked specifically how to solve this problem using joins.
That said, I think the traditional ActiveRecord approach to this problem is to use your models and associations.
You can add a user_name method to the TodoList model like this:
class TodoList < ApplicationRecord
belongs_to :user
def user_name
user.name
end
end
Now, any TodoList instance will return the name of its associated User regardless of how it was queried.
To avoid an n + 1 query scenario, you can use preload(:user) when querying TodoLists. For example, this code will make 2 queries to get the last 10 TodoLists and their associated Users.
TodoList.preload(:user).last(10)
I want to show a text summary for a model in a Rails application.
Currently I'm doing it like this:
class ServiceOrder < ApplicationRecord
has_many :items, class_name: 'ServiceOrderItem',
dependent: :destroy,
inverse_of: :service_order
def link_text
items.left_outer_joins(:product)
.select("string_agg(coalesce(products.description, service_order_items.description), '; ') as description")
.group("service_order_items.service_order_id")
.map(&:description)
.first
end
end
class ServiceOrderItem < ApplicationRecord
belongs_to :service_order, inverse_of: :items
belongs_to :product, optional: true
end
class Product < ApplicationRecord
end
What bothers me is that I'm trying to select a single value, and not a model.
This query does return a "fake" model and extract the value I want, but it's kind of hacky:
Add proper relations I need
items.left_outer_joins(:product)
Add the select value I want
.select("string_agg(coalesce(products.description, service_order_items.description), '; ') as description")
Add group by clause
.group("service_order_items.service_order_id")
Execute the query and extract the description of the "fake" model returned
.map(&:description)
I know this query only returns a single result, but it builds an array with all results, so I extract the single result out of the array
.first
The query I want is this:
select string_agg(coalesce(products.description, service_order_items.description), '; ')
from service_order_items
left outer join products on service_order_items.product_id = products.id
where service_order_items.service_order_id = :id
group by service_order_items.service_order_id;
And this is the query I'm generating, the problem is that the result is enclosed in a model object, then I transform it into an array and then I extract the value I want.
So, how do I tell active record to select a single raw value and not a list of models?
By the way, adding .first before .map doesn't work because it includes an order by in the executed SQL that I can't have (order by service_order_items.id).
The schema:
create_table "products", force: :cascade do |t|
t.integer "organization_id"
t.string "code"
t.string "description"
t.string "brand"
t.string "unit_of_measure"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.decimal "selling_price"
t.index ["organization_id"], name: "index_products_on_organization_id", using: :btree
end
create_table "service_order_items", force: :cascade do |t|
t.integer "service_order_id"
t.decimal "quantity"
t.string "description"
t.integer "product_id"
t.decimal "unit_price"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["product_id"], name: "index_service_order_items_on_product_id", using: :btree
t.index ["service_order_id"], name: "index_service_order_items_on_service_order_id", using: :btree
end
create_table "service_orders", force: :cascade do |t|
t.integer "organization_id"
t.text "description"
t.integer "state_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.integer "customer_id"
t.integer "sequential_id"
t.date "start_date"
t.date "end_date"
t.index ["customer_id"], name: "index_service_orders_on_customer_id", using: :btree
t.index ["organization_id"], name: "index_service_orders_on_organization_id", using: :btree
t.index ["state_id"], name: "index_service_orders_on_state_id", using: :btree
end
New answer
The need to use the description on service_order_items if there isn't a product makes this a little tricky. If you want to keep your custom SQL, it should be possible to use pluck with the same text as your select (minus the as description part):
def link_text
items.left_outer_joins(:product)
.group("service_order_items.service_order_id")
.pluck("string_agg(coalesce(products.description, service_order_items.description), '; ')")
.first
end
You also mentioned that you couldn't use first before map because it introduced an undesired order; you could try using take instead of first to avoid that, in which case you wouldn't need pluck.
Note that in either case you're introducing some dependencies on the table names, which could cause problems in more complex queries that require table aliases. If you want to go for less custom SQL,
the most direct way I can think of is to add the following method (probably with a name that better fits your application) to ServiceOrderItem:
def description_for_link_text
product.try(:description) || description
end
Then in ServiceOrder:
def link_text
items.includes(:product).map(&:description_for_link_text).join('; ')
end
The includes(:product) should avoid the N+1 issue where you do one query to get the items and then another query for each product. If you have a page that's displaying this text for multiple service orders, you have to deal with another level of this; often you have to declare a whole bunch of tables in includes even if they're declared in the link_text method.
service_orders = ServiceOrder.some_query_or_scope.includes(items: :product)
service_orders.each { |so| puts so.link_text }
If you do this, I don't think you actually have to have the includes in link_text itself, but if you removed it from there and you called link_text in any other situation, you'd get the N+1 issue again.
Original answer
I'm a bit confused by how your schema fits together: do service_orders and items have a one-to-many relationship, or a many-to-many relationship? How does products relate to items? And I don't have quite enough reputation to comment to ask.
In general, you can use pluck to get an array of values with just the attributes you want. I don't know off the top of my head if it works on virtual attributes, but you may be able to define has_many :through relationships so that you don't need to define string_agg(products.description, '; ') as description to join the strings together. That is, if your ServiceOrder model is able to have a products association like:
has_many :items
has_many :products, through: :items
then you could then just define link_text as products.pluck(:description).join("; "). You may need to play around with your has_many :through definition in order to get it to work right with your schema. Also, doing it this way does mean you have to watch out for potential N+1 query issues; see the Rails guide section on eager loading for how to address that.
I am trying to create a many to many relationship between my groups table and my keywords table.
When I am in my controller I cant do Keyword.groups or Group.keywords as I get a no method error. I have checked Group.methods and I only have these related methods
"before_add_for_groups_keywords",
"before_add_for_groups_keywords?",
"before_add_for_groups_keywords=",
"after_add_for_groups_keywords",
"after_add_for_groups_keywords?",
"after_add_for_groups_keywords=",
"before_remove_for_groups_keywords",
"before_remove_for_groups_keywords?",
"before_remove_for_groups_keywords=",
"after_remove_for_groups_keywords",
"after_remove_for_groups_keywords?",
"after_remove_for_groups_keywords=",
"before_add_for_keywords",
"before_add_for_keywords?",
"before_add_for_keywords=",
"after_add_for_keywords",
"after_add_for_keywords?",
"after_add_for_keywords=",
"before_remove_for_keywords",
"before_remove_for_keywords?",
"before_remove_for_keywords=",
"after_remove_for_keywords",
"after_remove_for_keywords?",
"after_remove_for_keywords=",
Where as Keyword.methods gives me these
"before_add_for_keywords_groups",
"before_add_for_keywords_groups?",
"before_add_for_keywords_groups=",
"after_add_for_keywords_groups",
"after_add_for_keywords_groups?",
"after_add_for_keywords_groups=",
"before_remove_for_keywords_groups",
"before_remove_for_keywords_groups?",
"before_remove_for_keywords_groups=",
"after_remove_for_keywords_groups",
"after_remove_for_keywords_groups?",
"after_remove_for_keywords_groups=",
"before_add_for_groups",
"before_add_for_groups?",
"before_add_for_groups=",
"after_add_for_groups",
"after_add_for_groups?",
"after_add_for_groups=",
"before_remove_for_groups",
"before_remove_for_groups?",
"before_remove_for_groups=",
"after_remove_for_groups",
"after_remove_for_groups?",
"after_remove_for_groups=",
My models
has_and_belongs_to_many :keywords
has_and_belongs_to_many :groups
My db schema is the following
create_table "groups", force: :cascade do |t|
t.integer "member_id"
t.string "name"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
add_index "groups", ["member_id"], name: "index_groups_on_member_id", using: :btree
create_table "groups_keywords", id: false, force: :cascade do |t|
t.integer "group_id"
t.integer "keyword_id"
end
add_index "groups_keywords", ["group_id", "keyword_id"], name: "index_groups_keywords_on_group_id_and_keyword_id", unique: true, using: :btree
create_table "keywords", force: :cascade do |t|
t.string "keyword"
t.string "keyword_hash"
t.datetime "checked_at"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
Associations are defined for instances.
Keyword.groups indeed does not exist (as a class method).
However, Keyword#groups, as in Keyword.first.groups, does work. The reason for that is that Keyword.first returns an instance of Keyword class, which Keyword class itself is not.
A better way to implement a many to many relationship is with the has_many :through idiom:
class Keyword
has_many :keyword_groups
has_many :groups, :through => :keyword_groups
class Group
has_many :keyword_groups
has_many :keywords, :through => :keyword_groups
class KeywordGroup
belongs_to :keyword
belongs_to :group
This will give you flexibility in the future if you need to extend your join model.
When you do Keyword.group(1), it doesn't do what you think it is doing. It just finds all the keywords and groups them by 1.
SELECT * "keywords".* FROM "keywords" GROUP BY 1
Indeed, you can't actually call these kind of method on Class level, associations are accessed through instance level. And as you have defined them has_and_belongs_to_many, you will have to be plural about them, not single.
keyword = Keyword.first
keyword.groups # Not 'group', and on instance level
schema.rb:
ActiveRecord::Schema.define(version: 20150324012404) do
create_table "groups", force: :cascade do |t|
t.string "title"
t.integer "teacher_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
create_table "groups_students", id: false, force: :cascade do |t|
t.integer "group_id"
t.integer "student_id"
end
add_index "groups_students", ["group_id"], name: "index_groups_students_on_group_id"
add_index "groups_students", ["student_id"], name: "index_groups_students_on_student_id"
create_table "users", force: :cascade do |t|
t.string "email", default: "", null: false
t.string "encrypted_password", default: "", null: false
t.string "reset_password_token"
t.datetime "reset_password_sent_at"
t.datetime "remember_created_at"
t.integer "sign_in_count", default: 0, null: false
t.datetime "current_sign_in_at"
t.datetime "last_sign_in_at"
t.string "current_sign_in_ip"
t.string "last_sign_in_ip"
t.datetime "created_at"
t.datetime "updated_at"
t.boolean "admin", default: false
t.string "type"
t.integer "group_id"
end
add_index "users", ["email"], name: "index_users_on_email", unique: true
add_index "users", ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true
end
group.rb:
class Group < ActiveRecord::Base
belongs_to :teacher
has_and_belongs_to_many :students
end
student.rb:
class Student < User
has_and_belongs_to_many :groups
end
I could have set a simple belongs_to and a has_many relationship between the student and group models, but I want students to be able to belong to more than one group, so I set up a HABTM association and corresponding join table.
I think I that right?
The question is, how do I, in the console, set a Student to belong to more than one group?
I have setup a User with 'type: Student' and I have two Groups. So...
In the console I do:
student = Student.first
Then, I want to set 'student' to belong to both Groups, but I don't know how to do this.
To set it to belong to one group I can do:
student.update_attributes(group_id: 1)
But how do make it belong to both groups? It would have two group_id's wouldn't it? I don't know how to set this.
If you need to see any of the other files, it's the 'handcode' branch here:
https://github.com/Yorkshireman/sebcoles/tree/handcode
The answers others have already provided are correct. But if you're working with id's you can also do something like this
student = Student.first
student.group_ids = 1,2,3,4
You don't need to set group_id for the User, the association is handled by the join table and the HABTM statement. You should remove group_id from the users table in the schema.
From memory you should be able to do something like this:
student = Student.first
groups = Group.all
student.groups << groups
student.save
See the active record guide on HABTM associations - specfically 4.4.1.3
Instead of habtm, just use the normal through and your life becomes easy. Make sure an id is generated for the association table (remove id:false)
create_table "group_students", force: :cascade do |t|
t.integer :group_id, nil:false
t.integer :student_id, nil:false
end
class Group < ActiveRecord::Base
has_many :group_students, dependent: :destroy, inverse_of :group
has_many :students, through :group_students
end
class Student < ActiveRecord::Base
has_many :group_students, dependent: :destroy, inverse_of :student
has_many :groups, through: :group_students
end
class GroupStudent < ActiveRecord::Base
belongs_to :group,
belongs_to :student
validates_presence_of :group, :student
end
Group.last.students << Student.last
or..
Student.last.groups << Group.last
Student.last.groups = [Group.find(1), Group.find(2)]
etc....
Ok, so it took me 3 days of all kinds of pain to work this out.
There was nothing wrong with my original code, except that I needed to remove the group_id from the user table.
roo's answer was correct, except that using 'group' as a variable name in the console confused Rails. This had led me to believe there was something wrong with my code, but there wasn't. You learn the hard way.
So, Students can be pushed into Groups like this:
To push a student into one group:
student = Student.first
OR
student = Student.find(1)
(or whatever number the id is)
group1 = Group.first
OR
group1 = Group.find(1)
student.groups << group1
To push into multiple groups (which was the original goal of this whole debacle:
student = Student.first
OR
student = Student.find(1)
allclasses = Group.all
student.groups << allclasses
To view your handywork:
student.groups
Works beautifully. The only problem I can see with my code is that it's possible to push the same student into a group twice, resulting in two duplicates of that student in one group. If anyone knows how to prevent this happening, I'm all ears.