Implementing model subclasses - the correct way - ruby-on-rails

I have an Event model and I want to create two subclasses of it: FixedEvent and TasksEvent
Event model has these attributes:
uid :string
title :string
starts_at :datetime
ends_at :datetime
FixedEvent inherits from Event and also has this attribute:
all_day :boolean
TasksEvent inherits from Event and has these attributes:
task_id :integer
occurrence :integer
(Occurrence attribute is a special way for tasks to say: Do this task two/three/x times. Occurrence represents which occurrence of the task this is (e.g. this is second time user is performing the task))
I spent the whole day yesterday reading about Single Table Inheritance and Polymorphic Associations and I'm still not 100% sure what's the correct way to implement this in Rails.
Single Table Inheritance leaves me with lot of null values in my database as I'll end up having one table with: uid, title, starts_at, ends_at, all_day, task_id, occurence, type.
Will this behaviour make server response slower as rails will fetch more (null) data for every event query?
On the other hand, Polymorphic Associations look more like I'm adding some extra functionality to model, rather than subclassing it. Also it creates more tables (three in this case) in the db:
events:
id, uid, title, starts_at, ends_at,
created_at, updated_at,
event_type_id, event_type_type
(suggest better naming for type if something comes to your mind)
fixed_events:
id
all_day
created_at
updated_at
tasks_events:
id
task_id
occurrence
created_at
updated_at
Will this behaviour make server response slower as rails will have to do several db joins every time I want to fetch all FixedEvent/TasksEvent attributes?
Also, how can I create new ActiveRecord objects using STI and/or Polymorphic Associations?
I tried something like this for Polymorphic Association:
def new
#fixed_event = FixedEvent.new
#fixed_event.build_event
respond_to :html
end
And then in form_for:
= f.fields_for :event do |event|
.field
= event.label :title
= event.text_field :title
.field
= event.label :starts_at
= event.datetime_select :starts_at
.field
= event.label :ends_at
= event.datetime_select :ends_at
.field
= event.label :description
= event.text_field :description
.field
= event.label :uid
= event.text_field :uid
I had to add these two things in FixedEvent and TasksEvent and it worked:
attr_accessible :event_attributes
accepts_nested_attributes_for :event
Is this the correct way to do it or STI is better (or any other solution)?

You want to go with STI as it is simple and will be much faster than loading associations. Loading a bunch of null values is nothing to be concerned about, performance wise. On the other hand, joins (or even eager loading) is much more likely to cause performance problems once your events table contains tens of thousands of entries.
If selecting all of the columns ever becomes a problem for you, you can always just select a subset of all columns with:
FixedEvent.select('foo, bar').all
It seems like you already understand STI so I don't have to teach you how to do it here. It's simple, really—just create the "events" table with "type" column, and Event class, then subclass it.

Related

SELECT "sum, count, fields" with 2 tables joined

I would like to convert this query using symbols and table_name cause my DB isn't in rails standard.
SELECT v.taux, sum(v.basevat) as tax, sum(v.valuevat) as amountvat, count(t.id)
FROM vatsomething as v
INNER JOIN somethingreceipt as t
ON v.uid_receipt = t.uid
WHERE t.cancelled = 0
GROUP BY v.taux
ORDER BY v.taux ASC;
class Vat < ActiveRecord::Base
self.table_name = "vatsomething"
alias_attribute :base, :basevat
alias_attribute :value, :valuevat
alias_attribute :rate, :taux
belongs_to :receipt, foreign_key: 'uid_receipt', primary_key: 'uid'
end
class Receipt < ActiveRecord::Base
self.table_name = "somethingreceipt"
has_many :item, foreign_key: 'uid_receipt', primary_key: 'uid'
end
I don't have the choice, if I divide this query to 4 queries, it's too slow.
I tried some queries :
Vat.joints(:receipt).
select(:rate, sum(:base), sum(:value), count(Receipt.table_name.:id) ).
where(Receipt.table_name => hash_of_conds.rejectblank)
I've tried quickly with pluck but I don't know if I can use symbols.
I understand that my query is really hard and the non standard db doesn't help.
Maybe, I'll have to use const_get...
Can you help me ?
Thank you.
Unfortunately you cannot use the more railsy methods very easily with legacy databases in rails. Your queries will have to end up being more SQL for this to function correctly. (I have run into this a lot with people that want to keep their DB but run it on rails)
Have you tried this
Vat.joins(:receipt).where("somethingreceipt.cancelled = 0").
group("vatsomething.taux").
select("vatsomething.taux as rate,
sum(vatsomething.basevat) as tax,
sum(vatsomething.valuevat) as amountvat,
count(somethingreceipt.id) as receipt_count").
order("vatsomething.taux")
This should return your desired result.
You have to remember that aliasing methods does not alter the names in the table when running queries you will still have to use the legacy table names and column names.
You can then access the attributes of each object in the ActiveRecord::Relation though their as names e.g. (#rate,#tax,#amountvat,#receipt_count)
Scoping option great for legacy DB's to keep your specific queries consolidated inside the model making it easier to make changes without having to find these queries all over the application.
class Vat
scope :receipt_summary, ->(conditions){
joins(:receipt).where(conditions).
group("vatsomething.taux").
select("vatsomething.taux as rate,
sum(vatsomething.basevat) as tax,
sum(vatsomething.valuevat) as amountvat,
count(somethingreceipt.id) as receipt_count").
order("vatsomething.taux")
}
end
Then you can call Vat.receipt_summary("somethingreceipt.cancelled = 0") or even should be able to call Vat.receipt_summary({receipt:{cancelled:0}})

Assemble a complex SQL query using Activerecord::Relation and/or Arel with 'joins', 'as', and 'like'

I am building a rails 3.2 app using datatables (http://datatables.net) with client-side paging and filtering on most html tables and server-side paging and filtering on some other html tables. I want to do per-column filtering, which is super-easy for the client side tables, but I think I need to construct a sql query for the database to do per-column filtering for the server side tables. I closely followed the example from RailsCast #340 on datatables and got that working.
The challenge is doing sorting and filtering on a column that is really a foreign_key relation to another table. I don't want to sort and filter on the actual contents of the foreign_key values. I want to sort and filter on the '.to_s' values displayed for the linked objects (which is the semantics of using the client-side sort and filter feature). Here is an example:
class Address < ActiveRecord::Base
attr_accessible :city, :line1, :line2, :state, :zip
has_many :people
def to_s
[line1, line2, city, state, zip].join(' ')
end
end
class Person < ActiveRecord::Base
attr_accessible :address, :name
belongs_to :address
end
so the view displaying the people list has two columns, for name and address
<td><%= p.name %></td>
<td><%= p.address %></td>
and what appears in the index table is
John Smith | 1234 Main St Anywhere City AA 12345
so with client-side sorting and filtering I can search for 'Anywhere' in the address column and get all the rows with that term in the address field. Doing the same thing on the server-side seems much more difficult. I think I'm trying to assemble a sql query that looks something like:
select * from people
join address on people.address_id = address.id
where concat(address.line1,
address.line2,
address.city,
address.state,
address.zip) as spec_address like query_term
order by spec_address
(This is not necessarily correct SQL code.)
I've looked at both the ActiveRecord Query Rails guide and anything I could find on Arel without success.
You can do this with a scope on Address which is then merged into the Person query.
class Address < ActiveRecord::Base
scope :anywhere, lambda{|search|
attrs = [:line1, :line2, :city, :state, :zip]
where(attrs.map{|attr| "addresses.#{attr} LIKE :search"}.join(' OR '), search: "#{search}%").
order(*attrs)
}
end
Person.joins(:address).merge(Address.anywhere(query_term))

ActiveRecord group by on a join

Really been struggling trying to get a group by to work when I have to join to another table. I can get the group by to work when I don't join, but when I want to group by a column on the other table I start having problems.
Tables:
Book
id, category_id
Category
id, name
ActiveRecord schema:
class Category < ActiveRecord::Base
has_many :books
end
class Book < ActiveRecord::Base
belongs_to :category
end
I am trying to get a group by on a count of categories. I.E. I want to know how many books are in each category.
I have tried numerous things, here is the latest,
books = Book.joins(:category).where(:select => 'count(books.id), Category.name', :group => 'Category.name')
I am looking to get something back like
[{:name => fiction, :count => 12}, {:name => non-fiction, :count => 4}]
Any ideas?
Thanks in advance!
How about this:
Category.joins(:books).group("categories.id").count
It should return an array of key/value pairs, where the key represents the category id, and the value represents the count of books associated with that category.
If you're just after the count of books in each category, the association methods you get from the has_many association may be enough (check out the Association Basics guide). You can get the number of books that belong to a particular category using
#category.books.size
If you wanted to build the array you described, you could build it yourself with something like:
array = Categories.all.map { |cat| { name: cat.name, count: cat.books.size } }
As an extra point, if you're likely to be looking up the number of books in a category frequently, you may also want to consider using a counter cache so getting the count of books in a category doesn't require an additional trip to the database. To do that, you'd need to make the following change in your books model:
# books.rb
belongs_to :category, counter_cache: true
And create a migration to add and initialize the column to be used by the counter cache:
class AddBooksCountToCategories < ActiveRecord::Migration
def change
add_column :categories, :books_count, :integer, default: 0, null: false
Category.all.each do |cat|
Category.reset_counters(cat.id, :books)
end
end
end
EDIT: After some experimentation, the following should give you close to what you want:
counts = Category.joins(:books).count(group: 'categories.name')
That will return a hash with the category name as keys and the counts as values. You could use .map { |k, v| { name: k, count: v } } to then get it to exactly the format you specified in your question.
I would keep an eye on something like that though -- once you have a large enough number of books, the join could slow things down somewhat. Using counter_cache will always be the most performant, and for a large enough number of books eager loading with two separate queries may also give you better performance (which was the reason eager loading using includes changed from using a joins to multiple queries in Rails 2.1).

Using single model for multiple tables

I am wondering if it is possible to combine columns from of different tables and use it as one model in Rails. I have two tables below, one holds generic columns and other specialize columns.
posts
--------------
id
title
description
created_at
updated_at
jobs
--------------
post_id
category_id
job_type
duration
salary
In Rails model,
class Job < ActiveRecord::Base
#
end
On saving Job model should save columns in respective tables. I thought about using single table inheritance (STI) but look like I can't split columns in multiple tables with this approach.
Hello you just need to use accepts_nested_attributes_for, then you can fill column of post on saving jobs using the posts_attributes key.
Adding posts to job
job[posts_attributes] = [{ :title => "test", :description => "Lorem ipsum"}]
Deleting posts from job
job[posts_attributes = [{ :id:20, :_destroy => true}]
Hope that will help you ;)

mongoid batch update

I'd like to update a massive set of document on an hourly basis.
Here's the
fairly simple Model:
class Article
include Mongoid::Document
field :article_nr, :type => Integer
field :vendor_nr, :type => Integer
field :description, :type => String
field :ean
field :stock
field :ordered
field :eta
so every hour i get a fresh stock list, where :stock,:ordered and :eta "might" have changed
and i need to update them all.
Edit:
the stocklist contains just
:article_nr, :stock, :ordered, :eta
wich i parse to a hash
In SQL i would have taken the route to foreign keying the article_nr to a "stock" table, dropping the whole stock table, and running a "collection.insert" or something alike
But that approach seems not to work with mongoid.
Any hints? i can't get my head around collection.update
and changing the foreign key on belongs_to and has_one seems not to work
(tried it, but then Article.first.stock was nil)
But there has to be a faster way than iterating over the stocklist array of hashes and doing
something like
Article.where( :article_nr => stocklist['article_nr']).update( stock: stocklist['stock'], eta: stocklist['eta'],orderd: stocklist['ordered'])
UPDATING
You can atomically update multiple documents in the database via a criteria using Criteria#update_all. This will perform an atomic $set on all the attributes passed to the method.
# Update all people with last name Oldman with new first name.
Person.where(last_name: "Oldman").update_all(
first_name: "Pappa Gary"
)
Now I can understood a bit more. You can try to do something like that, assuming that your article nr is uniq.
class Article
include Mongoid::Document
field :article_nr
field :name
key :article_nr
has_many :stocks
end
class Stock
include Mongoid::Document
field :article_id
field :eta
field :ordered
belongs_to :article
end
Then you when you create stock:
Stock.create(:article_id => "123", :eta => "200")
Then it will automaticly get assign to article with article_nr => "123"
So you can always call last stock.
my_article.stocks.last
If you want to more precise you add field :article_nr in Stock, and then :after_save make new_stock.article_id = new_stock.article_nr
This way you don't have to do any updates, just create new stocks and they always will be put to correct Article on insert and you be able to get latest one.
If you can extract just the stock information into a separate collection (perhaps with a has_one relationship in your Article), then you can use mongoimport with the --upsertFields option, using article_nr as your upsertField. See http://www.mongodb.org/display/DOCS/Import+Export+Tools.

Resources