Rails Active Record query including delegated attributes - ruby-on-rails

Is it possible to make a query like this? (Pseudo-code)
u=User.includes(all_delegated_attributes_from_relationships).all
How?
Further explanation:
class User<ActiveRecord::Base
has_one :car
delegate :wheel, :brake, :motor, to: :car, prefix: true
end
and then:
u=User.includes(delegated_car_parts).all
#<user id: 1, car_id: 1, name: "John", car_wheel: "foo", car_motor: "bar", car_brake: false>
I know that this can sound a little odd but I have to add a feature to an old app to export all delegated attributes from a model to CSV and this model has 14 relationships and 300 delegations... I just learnt Demeter's law when I made this app...

Assuming wheel, break and motor are relationships on car, you can do this:
User.includes(:car => [:wheel, :brake, :motor]).all

There is no build-in method to do this. You could try sth like:
class User < ActiveRecord::Base
has_one :car
DelegatedCarMethods = [:wheel, :brake, :motor]
delegate *DelegatedCarMethods, to: :car, prefix: true
scope :with_car_delagations, lambda do
select_array = ["#{table_name}.*"]
select_array += DelegateCarMethods.map {|method| "#{Car.table_name}.#{method} AS car_#{method}"}
select(select_array.join(', ')).joins(:car)
end
end
But it isn't extremely pretty. Why do you need this? Calling user.wheel or user.motor doesn't feel right.

Related

How to design a database for police fines?

Using Rails 6 I am designing an application to manage police fines. A user can violate many articles, an article can have many letters and a letter can have many commas.
This is my implementation:
#models/fine.rb
class Fine < ApplicationRecord
has_many :violations
has_many :articles, through: :violations
has_many :letters, through: :violations
has_many :commas, through: :violations
end
#models/article.rb
class Article < ApplicationRecord
has_many :letters
has_many :violations
has_many :fines, through: :violations
end
#models/letter.rb
class Letter < ApplicationRecord
belongs_to :article
has_many :commas
has_many :violations
has_many :fines, through: :violations
end
#models/comma.rb
class Comma < ApplicationRecord
belongs_to :letter
has_many :violations
has_many :fines, through: :violations
end
#models/violation.rb
class Violation < ApplicationRecord
belongs_to :fine
belongs_to :article
belongs_to :letter, optional: true
belongs_to :comma, optional: true
end
When I print the fine in PDF I need to show violations: articles, letters and commas. I have difficulty creating a form to compile the fine because it is too deep. I am using Active Admin, when I create a new fine I want to associate many violations.
Violation example:
Violation.new
=> #<Violation id: nil, article_id: nil, fine_id: nil, letter_id: nil, comma_id: nil, note: nil, created_at: nil, updated_at: nil>
How can I create a form (using Active Admin, which uses Formtastic) to associate many violations to a fine? Example form:
Example (with sample data):
Violation.new fine: fine, article: a, letter: a.letters.last, comma: a.letters.second.commas.last
=> #<Violation id: nil, article_id: 124, fine_id: 66, letter_id: 10, comma_id: 4, note: nil, created_at: nil, updated_at: nil>
In my humble opinion, your question is rather vague and difficult to answer based only on the provided information. Since I can't produce an answer that will definitely solve your issue, allow me to try and point you in the right direction.
Rendering the form
First let's understand the problem here: you're trying to create an association record in a nested resource form.
You need to customize the form for Fine to include a form for each violation. Look at how ActiveAdmin handles nested resources. It should be something like:
ActiveAdmin.register Fine do
form do |f|
inputs 'Violations' do
f.has_many :violations do |vf|
vf.input :article, as: :select, collection: Article.all
vf.input :letter, as: :select, collection: Letter.all
vf.input :comma, as: :select, collection: Comma.all
end
end
end
end
Put simply, this is the answer to your question "How can I create a form (using Active Admin, who use Formtastic) to associate many violations to a Fine?".
Caveats
As you probably already noticed, there are a couple of problems with this approach.
First, it is nothing like your example. You can easily change things for Formtastic to add the check-boxes by using as: :check_boxes, but you'll find the check-boxes are not organized as you want with that pretty indentation. As far as I know, there is no way for you to do this with Formtastic. Instead, I believe you would have to use a partial.
Using a partial you can easily go through the articles, and render a check-box for each of them and go through each one's letters, and so on. However, bear in mind this form will require you to customize the controller so it understands each of these check-boxes and creates the respective violations. Not as straight forward.
Second, there is nothing enforcing the data integrity here. One could select an article, the letter of another one, and the comma of a third one (by the way, I hope you have a validation to protect you from this). To have the form dynamically change, so only the letters of a given article are shown after its selection, and same thing for the commas, would require some client-side logic. Not worth the trouble if you ask me.
Conclusion
Your question is far from simple and obvious, both to answer and to solve. One option you always have is a custom set of routes for managing such resources outside ActiveAdmin. Remember, tools like this are only as valuable as the work they take from you. If you're having to fight it, better to just step out of each other's way.
Hope this helps, in any way.
Solved:
f.has_many :violations do |vf|
vf.input :article, as: :select, include_blank: false, collection: options_for_select(Article.all.map {|article| [article.number, article.id, { :'data-article-id' => article.id, :'data-letters' => article.letters.map(&:id).to_json }]})
vf.input :letter, as: :select, collection: options_for_select(Letter.all.map {|letter| [letter.letter, letter.id, { :'hidden' => true, :'data-letter-id' => letter.id, :'data-article-id' => letter.article.id, :'data-commas' => letter.commas.map(&:id).to_json }]})
vf.input :comma, as: :select, collection: options_for_select(Comma.all.map {|comma| [comma.number, comma.id, { :'hidden' => true, :'data-comma-id' => comma.id, :'data-letter-id' => comma.letter.id }]})
end
And with a bit of javascript:
$(document).on('has_many_add:after', '.has_many_container', function (e, fieldset, container) {
selects = fieldset.find('select');
article_select = selects[0];
letter_select = selects[1];
comma_select = selects[2];
$(article_select).on("change", function () {
$(letter_select).prop('selectedIndex', 0);
$(comma_select).prop('selectedIndex', 0);
$("#" + letter_select.id + " option").prop("hidden", true);
$("#" + comma_select.id + " option").prop("hidden", true);
letters = $(this).find(':selected').data('letters');
$.each(letters, function (index, id) {
$("#" + letter_select.id + " option[data-letter-id='" + id + "']").removeAttr("hidden");
});
});
$(letter_select).on("change", function () {
$(comma_select).prop('selectedIndex', 0);
$("#" + comma_select.id + " option").prop("hidden", true);
commas = $(this).find(':selected').data('commas');
$.each(commas, function (index, id) {
$("#" + comma_select.id + " option[data-comma-id='" + id + "']").removeAttr("hidden");
});
});
});
I show all Articles, Letters and Commas in the selectbox. Initially Commas and Letters are hidden, then when a User click an Article the Letter's selectbox show only the related Letters.
The Commas code works same as Letters.
After I can add some validations in the Violation model.

find a user by model associations rails

I have a model Camera in which
belongs_to :user, :foreign_key => 'owner_id', :class_name => 'EvercamUser'
i have asscociation like this. when i do Camera.first
#<Camera id: 6, created_at: "2013-12-12 17:30:32", updated_at: "2015-11-19 10:19:33", exid: "dublin-rememberance-floor2", owner_id: 4, is_public: true
i can get owner id, is there any way to create such function that , along side getting owner id, i can get the data which linked with this id for example at id = 4
#<EvercamUser id: 4, created_at: "2013-12-12 16:43:46", updated_at: "2015-04-16 15:23:19", firstname: "Garrett", lastname: "Heaver", username: "garrettheaver"
this user is present, what if when i do Camera.first then instead of OnwerID, how can i get the owners Name?
Any help will be appreciated!
how can i get the owners Name
You'd call the associative object on the Camera object:
#camera = Camera.find x
#user = #camera.user
#user.name #-> outputs name of associated user object
... this will allow you to call the attributes of the child object on it: #camera.user.name or #camera.user.email, etc
Off topic, but I always include a reference to delegate for this type of issue; it avoids the law of demeter (where you're using more than one point to access data).
This would allow you to use:
#app/models/camera.rb
class Camera < ActiveRecord::Base
belongs_to :user, foreign_key: :owner_id, class_name: 'EvercamUser'
delegate :name, to: :user, prefix: true #-> #camera.user_name
end
#camera = Camera.find x
#camera.user_name #-> outputs the user's name on the camera object (not user object)
To give you some context, Rails uses ActiveRecord to invoke/create objects for you.
In line with the object orientated nature of Rails, ActiveRecord is known as an ORM (Object Relationship Mapper). This basically allows you to create an object through ActiveRecord, and if it is associated to another (as Rails does with its associations), it will append the associated object onto the parent.
Thus, when you're asking about calling owner_id, you're referring to the foreign_key of the association (the database column which joins the two tables together):
What you need is to reference the associated object, which I've detailed above.
What about using join here?
Camera.all.joins(:evercamusers)
Camera.where(:id => 1).joins(:users).first
Note: I'm a bit unsure if the correct parameter should be ":users" or ":evercamusers"
http://apidock.com/rails/ActiveRecord/QueryMethods/joins
You could also add methods to your class to do this.
class Camera < ActiveRecord::Base
belongs_to :user, :foreign_key => 'owner_id', :class_name => 'EvercamUser'
def firstname
self.user.firstname
end
end
When you try to output data from Camera like this:
#<Camera id: 6, created_at: "2013-12-12 17:30:32", updated_at: "2015-11-19 10:19:33", exid: "dublin-rememberance-floor2", owner_id: 4, is_public: true
It won't show. But if you call the method like this, it should work:
Camera.first.firstname # "Garrett"
Also, if JSON is acceptable you could override the as_json method.
def as_json(options={})
{ :firstname => self.user.firstname }
end
Then call it with
Camera.first.as_json
If you need to do it with all, simply loop it
Camera.all.each { |c| puts c.firstname }

Making a has_many relation avoiding childs to store paretn's id

class User
include Mongoid::Document
has_many :favorites, class_name: "Item"
end
class Item
include Mongoid::Document
belongs_to :user, dependent: :nullify
end
I want that the users have an array of favorites but in Item collection, the user_id is not stored. Is the approach I followed correct?
If I try to access a user favorites as User.last.favourites or try to add a favorite to a user, it takes for ever. Why is this?
Thanks
I believe you are missing the embedded_in :user on your Item class
class User
include Mongoid::Document
has_many :favorites, class_name: "Item"
end
class Item
# class is all lower case
include Mongoid::Document
# remove the relation to the user form the item
# so that it cannot save the user_id
end
the previous code should work, and won't allow you to save the user_id in a favorite.
so this code should work user.last.favorites #=> [Array of favorites]
but this code will through an exception user.last.favorites.last.user #=> method user not found
why it takes forever ?! I cannot judge unless i've seen the logs.
also don't go for the embedded solution, the embedded collection is only accessible from the parent... and i think this is not what you want to achieve...
in a simpler words: if you have a favorite that is called 'rails',
using the embedded solution described in the other answer would result in the following behaviour
p Favorites.all.to_a
#=> []
p user.first.favorites.first
#=> <Favorite id: 1, name: rails>
p user.last.favorites.first
#=> <Favorite id: 100, name: rails>
if you noticed:
the collections is not query-able except from the parent that embeds it.
the favorites collections is unique per user. 2 different users cannot share the same embedded document ( as in the previous code, both first and last user has the same favorite (what you intended) but they are actually 2 completely different objects.
the same code using the has_many relation would result in the following
p Favorites.all.to_a
#=> [<<Favorite id: 1, name: rails>]
p user.first.favorites.first
#=> <Favorite id: 1, name: rails>
p user.last.favorites.first
#=> <Favorite id: 1, name: rails>
# this is pseudocode but you will get the idea
user.last.favorites.first.name = `rails4` # then save
p user.first.favorites.first
#=> <Favorite id: 1, name: rails4>
p user.last.favorites.first
#=> <Favorite id: 1, name: rails4>

Polymorphic subclass incorrectly queries STI sublass

I have an STI subclass, SoftCredits::EventBrite < SoftCredit, that has a function called on creation of SoftCredit
def create_activity
options = {
owner: person,
created_at: additional_data['created_at'],
updated_at: additional_data['updated_at'],
trail_item_type: "Activities::EventBriteRegistration",
trail_item_id: self.id
}
Activity.create(options)
end
Activity model is polymorphic
belongs_to :trail_item, :polymorphic => true
The Activities::EventBriteRegistration model declaration looks like so:
class Activities::EventBriteRegistration < SoftCredit
def self.default_scope
unscoped
end
end
My issue is when I have an Activity type of Activities::EventBriteRegistration and I query on it event_brite_registration_activity.trail_item, the query is construct like so:
SELECT "soft_credits".* FROM "soft_credits" WHERE "soft_credits"."type" IN ('Activities::EventBriteRegistration') AND "soft_credits"."id" =[id] LIMIT 1.
Of course, there is no type Activities::EventBriteRegistration for SoftCredits and I am confused how I should construct this so when I do event_brite_registration_activity.trail_item, it queries on SoftCredit to find the correct soft credit.

capturing the right model behavior, do I use serialize() or a model relationship?

I have a model called sites. A site will have several adjacent sites (and also be an adjacent site for other sites).
I am trying to determine how best to capture the list of adjacencies.
I have considered creating a text field in the site model and using serialize to store/retrieve the array of all of the adjacent sites. The problem with this is that I'd be violating DRY since there'd be no real relationship formed between the adjacent sites, and thus would have to store the list of adjacent sites for each site individually.
I started digging through some of the online docs for the has_and_belongs_to_many relationship, but in the examples I've found the relationship seems to always be between two different types of objects. Can I have a has_and_belongs_to_many relationship with the same object?
so:
class Site < ActiveRecord::Base
has_and_belongs_to :sites
end
or do I need to create a seperate table for adjacent sites?
Notice that the solution you found works in one direction only:
>> Site.last.friends
[]
>> Site.last.friends << Site.first
[#<Site id: 1, name: "First", description: "The First", created_at: "2009-09-08 21:15:09", updated_at: "2009-09-08 21:15:09">]
>> Site.last.friends
[#<Site id: 1, name: "First", description: "The First", created_at: "2009-09-08 21:15:09", updated_at: "2009-09-08 21:15:09">]
>> Site.first.friends
[]
If you want it to work two ways, you can use something like:
class Site < ActiveRecord::Base
has_and_belongs_to_many :l_adjacent_sites, :class_name => 'Site', :join_table => 'sites_sites', :foreign_key => 'l_site_id', :association_foreign_key => 'r_site_id'
has_and_belongs_to_many :r_adjacent_sites, :class_name => 'Site', :join_table => 'sites_sites', :foreign_key => 'r_site_id', :association_foreign_key => 'l_site_id'
end
But the arcs are directed:
>> Site.first.r_adjacent_sites
[]
>> Site.last.r_adjacent_sites < Site.first
[#<Site id: 1, name: "First", description: "The First", created_at: "2009-09-08 21:15:09", updated_at: "2009-09-08 21:15:09">]
>> Site.last.r_adjacent_sites
[#<Site id: 1, name: "First", description: "The First", created_at: "2009-09-08 21:15:09", updated_at: "2009-09-08 21:15:09">]
>> Site.first.l_adjacent_sites
[#<Site id: 4, name: "Fourth", description: "The fourth", created_at: "2009-09-08 21:48:04", updated_at: "2009-09-08 21:48:04">]
If what you want to represent is directed arcs, you'll be fine; I haven't figured yet a solution for nondirected arcs (apart from mysite.l_adjacent_sites + mysyte.r_adjacent_sites]).
EDIT
I tried to hack something to obtain a adjacent_sites named_scope or the like, but couldn't find anything; also, I'm not sure that a general solution (allowing you to filter results adding more conditions) actually exists.
Since doing l_adjacent_sites + r_adjacent_sites forces the (two) queries execution,
I can only suggest something like:
def adjacent_sites options={}
l_adjacent_sites.all(options) + r_adjacent_sites.all(options)
end
This should allow you to do things like:
#mysite.adjacent_sites :conditions => ["name LIKE ?", "f%"]
There are still issues, though:
Sorting will not work, that is, you'll get a halfsorted set, like [1, 3, 5, 2, 4, 6]. If you need to sort results; you'll have to do it in ruby.
Limit will only half-work: :limit => 1 will give you up to 2 results, as two queries will be executed.
But I'm positive that for most purposes you'll be ok.
It should be possible to do this, however it will involve playing about with the options on
has_and_belongs_to_many
You'll probably have a join table defined with a migration something along the lines of:
create_table :sites_associated_sites, :id => false do |t|
t.integer :site_id
t.integer :associated_site_id
end
Then in the model you'll need to setup has_and_belongs_to_many twice
class Site < ActiveRecord::Base
has_and_belongs_to_many :sites, :join_table => 'sites_associated_sites'
has_and_belongs_to_many :associated_sites, :class_name => 'Site',
:join_table => 'sites_associated_sites'
end
This answer should be taken with a pinch of salt as this is more loud thinking than anything else, but definitely worth having a read up on the has_and_belongs_to_many method in the API: http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html#M001836
I dug some more and found a good resource for this. The example they used is if you have a class of users, and users can have friends who are also users, but you don't want to create a separate table of friends.
What the author of the article did was create a join table with just the foreign keys, that allows the habtm relationship to exist without a third table.
Here's the link:
http://www.urbanpuddle.com/articles/2007/06/14/rails-mini-tutorial-user-habtm-friends
Some additional digging turned up a method of accomplishing Bidrectional Relationships in the reference book The Rails Way (page 227). Unfortunately there are two problems with the solution in the text. First it was incomplete, though a complete version was available on the book's bug tracker/errata page
class BillingCode < ActiveRecord::Base
has_and_belongs_to_many :related,
:join_table => 'related_billing_codes',
:foreign_key => 'first_billing_code_id',
:association_foreign_key = 'second_billing_code_id',
:class_name => 'BillingCode',
:insert_sql => 'INSERT INTO related_billing_codes (`first_billing_code_id`, `second_billing_code_id`) VALUES (#{id}, #{record.id}), (#{record.id}, #{id})',
:delete_sql => 'DELETE FROM related_billing_codes WHERE (`first_billing_code_id` = #{id} AND `second_billing_code_id` = #{record.id}) OR (`first_billing_code_id` = #{record.id} AND `second_billing_code_id` = #{id})'
end
The second problem is that the solution (which relies on an SQL statement inserting two records into the table within the same INSERT statement) is not supported by the default development database SQLite3.

Resources