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.
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
Question: How to save GraphQL query to my local/postgres database?
(let's assume I just want to save one or two fields -- I'll do more, but I just am not sure how to go about it to begin with.)
Background:
I have an embedded shopify app that works with webhooks to consume orders. I run a job, and the data is stored perfectly in my database.
I’ve set up a super simple app that makes a graphql query in a model. I’ve set it up so I can see the json object in a view. But am not 100% clear how I go from graphql api query -> response (in model - at least for now) -> save data attributes to my database
I am testing this as a new app. So, I am not splitting up models/controllers, etc., just using one model for the moment
I guess I am just a bit lost. Do I need to run a job? Or is this something I can do in the controller (or from the model).
Model
shop.rb
class Shop < ActiveRecord::Base
include ShopifyApp::ShopSessionStorage
has_many :orders
def api_version
ShopifyApp.configuration.api_version
end
session = ShopifyAPI::Session.new(domain: "bryanbeshore.myshopify.com", token: Shop.first.shopify_token, api_version: "2020-04")
ShopifyAPI::Base.activate_session(session)
client = ShopifyAPI::GraphQL.client
SHOP_NAME_QUERY = client.parse <<-'GRAPHQL'
{
shop {
name
}
orders(first: 100) {
edges {
node {
id
name
createdAt
shippingAddress {
address1
address2
city
province
provinceCode
zip
}
}
}
}
}
GRAPHQL
end
Controller
home_controller.rb
class HomeController < AuthenticatedController
def index
client = ShopifyAPI::GraphQL.client
#shop_orders = client.query(Shop::SHOP_NAME_QUERY).data.to_json
end
end
View
app/views/home/index.html.erb
<p><%= #shop_orders %></p>
Current Schema
ActiveRecord::Schema.define(version: 2020_05_06_181457) do
create_table "orders", force: :cascade do |t|
t.string "shopify_order_id", null: false
t.string "shopify_order_name", default: ""
t.datetime "shopify_order_created_at"
t.integer "shop_id"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
t.index ["shop_id"], name: "index_orders_on_shop_id"
t.index ["shopify_order_id"], name: "index_orders_on_shopify_order_id", unique: true
end
create_table "shops", force: :cascade do |t|
t.string "shopify_domain", null: false
t.string "shopify_token", null: false
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
t.index ["shopify_domain"], name: "index_shops_on_shopify_domain", unique: true
end
end
You are querying Shop orders, so I'd create an Order model / orders table, make sure it belongs to a shop.
rails g model Order payload:jsonb shop:references, for this example I'm just creating one jsonb field that we will dump the whole JSON object into.
rails db:migrate
Add belongs_to :shop in models/order.rb
Make sure that Shop model has this has_many :orders, usually rails add it for you
Now, when you get your JSON payload from Shopify, loop through it and create a new record for each order you got.
So in the method you use to query orders add this.
shopify_json_response.each do |item|
orders.create(payload: item)
end
More or less, that should do the trick.
You don't need a background job for this, however background jobs are ideal when you want to process data that doesn't need to be processed right away.
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)
Issue is I can't find why reference column id can't be inserted when create new record.
I have 3 table shop_plan, shop and app
Below is tables schema:
create_table "shop_plans", force: :cascade do |t|
t.string "name"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
create_table "shops", force: :cascade do |t|
t.string "url"
t.bigint "plan_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["plan_id"], name: "index_shops_on_plan_id"
end
create_table "apps", force: :cascade do |t|
t.bigint "shop_id"
t.binint "amount"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["app_id"], name: "index_apps_on_shop_id"
end
add_foreign_key "shops", "shop_plans", column: "plan_id"
add_foreign_key "apps", "shops"
And below is Model
class ShopPlan < ApplicationRecord
has_many :shop
end
class Shop < ApplicationRecord
belongs_to :shop_plan, class_name: 'ShopPlan', foreign_key: :plan_id
has_many :app
end
class App < ApplicationRecord
belongs_to :shop, class_name: 'Shop', foreign_key: :shop_id
end
There will be 1 default record added in seed.db for table shop_plan
ShopPlan.create(name: 'Basic')
ShopPlan and Shop are linked by plan_id column in Shop
Shop and App are linked by shop_id column in App
I pre-insert some value when user access index:
#basic_plan
#basicPlan = ShopPlan.where(name: "Basic").first
# if new shop registered, add to database
unless Shop.where(url: #shop_session.url).any?
shop = Shop.new
shop.url = #shop_session.url
shop.plan_id = #basicPlan.id
shop.save
end
This insert works well, however, when i run 2nd insert:
#shop= Shop.where(url: #shop_session.url).first
unless App.where(shop_id: #shop.id).any?
app = App.new
app.shop_id = #shop.id,
app.amount = 10
app.save
end
error occurs as somehow app.shop_id will not add in my #shop.id and it will return will error: {"shop":["must exist"]}
I even try hard-code app.shop_id =1 but it does not help and when I add in optional: true to app.db model, it will insert null
Appreciate if anyone can help point out why I get this error
EDIT: #arieljuod to be clear
1) I have to specific exact column class due to between Shop And Shop_Plan, i'm using a manual plan_id instead of using default shopplans_id columns.
2) I have update 1 column inside App and all that unless is just to do checking when debugging.
First of all, like #David pointed out, your associations names are not right. You have to set has_many :shops and has_many :apps so activerecord knows how to find the correct classes.
Second, you don't have to specify the class_name option if the class can be infered from the association name, so it can be belongs_to :shop and belongs_to :shop_plan, foreign_key: :plan_id. It works just fine with your setup, it's just a suggestion to remove unnecesary code.
Now, for your relationships, I think you shouldn't do those first any? new block manually, rails can handle those for you.
you could do something like
#basicPlan = ShopPlan.find_by(name: "Basic")
#this gives you the first record or creates a new one
#shop = #basicPlan.shops.where(url: #shop_session.url).first_or_create
#this will return the "app" of the shop if it already exists, and, if nil, it will create a new one
#app = #shop.app or #shop.create_app
I have found out the silly reason why my code does not work.
It's not because as_many :shops and has_many :app and also not because my code when creating the record.
It just due to silly comma ',' when creating new record in App at app.shop_id = #shop.id,, as I was keep switching between Ruby and JavaScript. Thank you #arieljuod and #David for your effort
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