I'm building an application where users are part of an Organisation. An organisation has many Lists, which in turn have many ListItems.
Now, I would like for admin users to be able to specify which attributes are available on list items, based on the organisation they belong to (or rather, on the organisation their list belongs to), without having to touch any code.
So far, when defining attributes that are not bound to a specific column in the database, I have used document_serializable, a nifty little gem (based on virtus) which serializes virtual attributes to a JSONB column in the db. I like this approach, because I get all of virtus' goodies (types, coercion, validations, etc.), and because data ends up sitting in a JSONB column, meaning it can be loaded quickly, indexed, and searched through with relative ease.
I would like to keep using this approach when adding these user-defined attributes on the fly. So I'd like to do something like:
class ListItem < ApplicationRecord
belongs_to :list
delegate :organisation, to: :list
organisation.list_attributes.each do |a, t|
attribute a, t
end
end
Where Organisation#list_attributes returns the user-defined hash of attribute names and their associated types, which, for example, might look like:
{
name: String,
age: Integer
}
As you might have guessed, this does not work, because organisation.list_attributes.each actually runs in the context of ListItem, which is an instance of Class, and Class doesn't have an #organisation method. I hope that's worded in a way that makes sense1.
I've tried using after_initialize, but at that point in the object's lifecycle, #attribute is owned by ActiveRecord::AttributeMethods::Read and not DocumentSerializable::ClassMethods, so it's an entirely different method and I can't figure out wether I can still access the one I need, and wether that would even work.
Another alternative would be to find the organisation in question in some explicit way, Organisation#find-style, but I honestly don't know where I should store the information necessary to do so.
So, my question: at the moment of instantiating (initializing or loading2) a record, is there a way I can retrieve a hash stored in a database column of one of its relations? Or am I trying to build this in a completely misguided way, and if so, how else should I go about it?
1 To clarify, if I were to use the hash directly like so:
class ListItem < ApplicationRecord
belongs_to :list
delegate :organisation, to: :list
{
name: String,
age: Integer
}.each do |a, t|
attribute a, t
end
end
it would work, my issue is solely with getting a record's relation at this earlier point in time.
2 My understanding is that Rails runs a model's code whenever a record of that type is created or loaded from the database, meaning the virtual attributes are defined anew every time this happens, which is why I'm asking how to do this in both cases.
at the moment of instantiating (initializing or loading) a record, is
there a way I can retrieve a hash stored in a database column of one
of its relations?
Yes. This is fairly trivial as long as your relations are setup correctly / simply. Lets say we have these three models:
class ListItem < ApplicationRecord
belongs_to :list
end
class List < ApplicationRecord
belongs_to :organisation
has_many :list_items
end
class Organisation < ApplicationRecord
has_many :lists
end
We can instantiate a ListItem and then retrieve data from anyone of its parents.
#list_item = ListItem.find(5) # assume that the proper inherited
foreign_keys exist for this and
its parent
#list = #list_item.list
#hash = #list.organisation.special_hash_of_org
And if we wanted to do this at every instance of a ListItem, we can use Active Record Callbacks like this:
class ListItem < ApplicationRecord
belongs_to :list
# this is called on ListItem.new and whenever we pull from our DB
after_initialize do |list_item|
puts "You have initialized a ListItem!"
list = list_item.list
hash = list.organisation.special_hash_of_org
end
end
But after_initialize feels like a strange usage for this kind of thing. Maybe a helper method would be a better option!
Related
I have a scenario where I generate reports from certain ActiveRecord models.
I have multiple roles in the application. Depending on the roles, I want to show or hide certain columns. The thing is as the number of screens/pages increase, keeping a track of these can be become a nightmare.
Is there a way in Rails, where, I can stop returning values for certain columns depending on certain conditions. For e.g. I will the object returned from a ActiveRecord.Where will have data for some columns masked depending on User's role.
You can do that using active record select method. Select only those attributes which current user role can access and pass to view.
For this you can create array of accessible feilds for paticular role in your initializer. For this create a initializer.rb file under config/initializers/. Add code something like:
ADMIN = ['feild1', 'feild2'..., 'feild10']
MANAGER = ['feild1', 'feild2'..., 'feild5']
USER = ['feild1', 'feild2', 'feild3']
in your action write code something like :
Model.select(eval(current_user.role.upcase))
In view you need to check if attribute exist in your retured activerecord or not. Otherview you will get ActiveModel::MissingAttributeError: for this:
object.has_attribute? 'att_name'
Or you can rescue it with nil
object.att_name rescue nil
I literally just wrote an answer about this - you'll probably benefit from it.
Model
It seems that if you want to return specific ActiveRecord data, there are certain ways to limit the attributes the class builds. More specifically, you can make certain methods "private" - preventing your model from returning them.
Although I'm not 100% sure on this, I can say that there are two "levels" to your question -- the database data & the model's construction. Although I don't have anything for the ActiveRecord side of things, the model can "privatize" certain attributes, preventing them from being available in other parts of your app.
A Rails model is a class - populated with attributes. This means you should be able to control which attributes are available by the Role your user is part of:
#app/models/role.rb
class Role < ActiveRecord::Base
#columns id | name | attributes | created_at | updated_at
#"attributes" can be used to assign an array
has_many :users, inverse_of: :role
end
#app/models/user.rb
class User < ActiveRecord::Base
belongs_to :role, inverse_of: :users
role.attributes.each do |attr|
private attr.to_sym
end
end
This will override the attributes pulled from the db, allowing you to determine which ones are available.
Of course, a very rudimentary procedure.
--
ActiveRecord
The best way around this will be to use ActiveRecord to specifically select the attributes / columns you want. To do this, I'm not sure of the absolute best way, but perhaps using a default_scope would be beneficial:
#app/models/user.rb
class User < ActiveRecord::Base
belongs_to :role
def attributes
case role_id
when "1"
attrs = []
when "2"
attrs = []
when "3"
attrs = []
end
end
default_scope (select: attributes)
end
Again, pretty rudimentary. I'd be interested in seeing a more integrated way of doing this.
Imagine this:
class House < ActiveRecord::Base
belongs_to :ground
delegate :elevation_in_meters, to: :ground
# attributes: stories, roof_type
end
class Ground < ActiveRecord::Base
has_one :house
# attributes: elevation_in_meters, geo_data
end
Then to eager load ground so that house.elevation_in_meters can be called without loading Ground I can do:
houses=House.includes(:ground).first(3)
The problem with this is, that the entire Ground object is actually instantiated with all attributes including the geo_data attribute - which I don't need in this case. The reason why I care is, that the query needs to be VERY performant, and geo_data is a pretty huge text field. I only need to read the delegated attributes, not write to them.
What approach could I take on eager loading the elevation_in_meters attribute from Ground without loading everything from Ground?
I'm on rails 4.1 btw
NOTE: Preferably I would like to have this eager loading behaviour by default for House, so that I do not need to specify it every time.
First off write a scope for the model you want to partially get and select the fields you like. Notice that I used the full name (with table name) and a string for the select. I'm not sure if you could just select(:elevation_in_meters,:geo_data) since I've copied this from our production example, and we use some joins with this scope that wont work without the table name. Just try it yourself.
class Ground < ActiveRecord::Base
has_one :house
attributes: elevation_in_meters, geo_data
scope :reduced, -> {
select('grounds.elevation_in_meters, grounds.geo_data')
}
end
With the scope present you can make a second belongs_to relation (don't be scared that it messes up your first one, since rails relations are basically just methods that are created for you), that calls the scope on your Ground model.
class House < ActiveRecord::Base
belongs_to :ground
belongs_to :ground_reduced, ->(_o) { reduced },
class_name: 'Ground', foreign_key: 'ground_id'
delegate :elevation_in_meters, to: :ground_reduced
# go for an additional delegation if
# you also need this with the full object sometimes
end
In the end you can just call your query like this:
houses = House.includes(:ground_reduced).first(3)
Technically it is not the proper answer to your question, since the Ground object is still instantiated. But the instance will only have the data you wanted and the other fields will be nil, so it should do the trick.
UPDATE:
As I just saw that you want to preferably have this behaviour as default, just add a scope for your House:
scope :reduced, -> { includes(:ground_reduced) }
You could then add this as your default scope, since your original relation will be untouched by this.
I know it's been a while but I just stumbled across this.
If you're only interested in the singular attribute you can also use a joins combined with a select and the attribute will magically be added to your House instance.
res = House.joins(:ground).select('houses.*, grounds.elevation_in_meters').first
res.elevation_in_meters # attribute is available on the object
To always have this attribute present, make it the default_scope for House, like so:
default_scope { joins(:ground).select('houses.*, grounds.elevation_in_meters') }
Depending on the nature of the tables you're joining you may need a distinct also.
I understand the concept of relational databases, primary/foreign keys, etc, however, I'm having trouble seeing the actual result of setting these properties up in my models. Do they generate helper functions or something similar? or do they simply work on a database level?
For example, if I have these two models (other properties omitted)
class Course < ActiveRecord::Base
has_and_belongs_to_many :schedules
has_many :sections
end
class Section < ActiveRecord::Base
belongs_to :course
end
I could simply get all sections for any given course like this:
Section.where(course_id: 1234)
However, I could do this without having set up the relations at all.
So my question is: Why do we do this?
Adding these methods let's you do things like this:
Section.find(5).course # <== returns a 'Course' model instance
Also it let's you join your queries more easily:
Section.joins(:course).where(course: {name: "English"}) # <== returns sections who have the 'english' course
Neither of these would be possible if you didn't set up the relations in the model.
Similarly:
Course.find(8).sections # returns an array of sections with 'course_id = 8'
It makes your calls more semantic, and makes things easier from a programmatic perspective :)
Relations are applied on instances of an object. So, these relations allow you to get related objects to an instance of another.
For example, say you have an instance of Section (called #section). You'd be able to get all Course objects for that section by doing:
#section.course if you have belongs_to :course set up.
Similarly, if you have an instance of Course, you can get all Section objects for that Course with:
#course.sections if you have has_many :sections.
TL;DR - these are helper scopes for instance variables of Course and Section.
New to Rails and Ruby and trying to do things correctly.
Here are my models. Everything works fine, but I want to do things the "right" way so to speak.
I have an import process that takes a CSV and tries to either create a new record or update an existing one.
So the process is 1.) parse csv row 2.) find or create record 3.) save record
I have this working perfectly, but the code seems like it could be improved. If ParcelType wasn't involved it would be fine, since I'm creating/retrieving a parcel FROM the Manufacturer, that foreign key is pre-populated for me. But the ParcelType isn't. Anyway to have both Type and Manufacturer pre-populated since I'm using them both in the search?
CSV row can have multiple manufacturers per row (results in 2 almost identical rows, just with diff mfr_id) so that's what the .each is about
manufacturer_id.split(";").each do |mfr_string|
mfr = Manufacturer.find_by_name(mfr_string)
# If it's a mfr we don't care about, don't put it in the db
next if mfr.nil?
# Unique parcel is defined by it's manufacturer, it's type, it's model number, and it's reference_number
parcel = mfr.parcels.of_type('FR').find_or_initialize_by_model_number_and_reference_number(attributes[:model_number], attributes[:reference_number])
parcel.assign_attributes(attributes)
# this line in particular is a bummer. if it finds a parcel and I'm updating, this line is superfulous, only necessary when it's a new parcel
parcel.parcel_type = ParcelType.find_by_code('FR')
parcel.save!
end
class Parcel < ActiveRecord::Base
belongs_to :parcel_type
belongs_to :manufacturer
def self.of_type(type)
joins(:parcel_type).where(:parcel_types => {:code => type.upcase}).readonly(false) unless type.nil?
end
end
class Manufacturer < ActiveRecord::Base
has_many :parcels
end
class ParcelType < ActiveRecord::Base
has_many :parcels
end
It sounds like the new_record? method is what you're looking for.
new_record?() public
Returns true if this object hasn’t been saved yet — that is, a record
for the object doesn’t exist yet; otherwise, returns false.
The following will only execute if the parcel object is indeed a new record:
parcel.parcel_type = ParcelType.find_by_code('FR') if parcel.new_record?
What about 'find_or_create'?
I have wanted to use this from a long time, check these links.
Usage:
http://rubyquicktips.com/post/344181578/find-or-create-an-object-in-one-command
Several attributes:
Rails find_or_create by more than one attribute?
Extra:
How can I pass multiple attributes to find_or_create_by in Rails 3?
I've got a legacy database schema which consists of objects similar to the following:
table=car
oid, something, something_else, ...
has many properties -> car_properties
table=car_properties
oid, car_id, keyname, value, ...
belongs to car
The Car object is actually (logically) a combination of the columns in the "car" table, and multiple rows in the "car_properties" table.
I'm looking at doing a parallel rewrite of the application which uses this schema, so I need some way to map this table schema back to a nice ActiveRecord object. Ideally I'd like each of the properties in the _properties table to be accessible as a method on the "Car" class, so I can change the underlying table later without breaking things.
I can statically define the methods in the Car class, but I want to ensure that the ActiveRecord magic works, so things like .save work, and I can change the underlying schema at a later date (realising this will probably be an outage to the application).
How would I go about doing this in ActiveRecord?
To Clarify:
Basically, I want the following to work
#car = Car.first
#car.something = something
#car.someprop = something
However in the above, #code.someprop is actually #car.properties.where( "keyname = ?", "someprop" ).value
Obviously I don't want to be doing a SQL Query every time for this though, so I'm looking for a nice way to do this.
Unless I'm underthinking it, it should be something as simple as:
class CarProperty < ActiveRecord::Base
set_primary_key :oid
belongs_to :car
end
class Car < ActiveRecord::Base
set_table_name :car
set_primary_key :oid
has_many :car_properties
accepts_nested_attributes_for :car_properties
end