Where is Person and not Person::Single - ruby

Lets say I have three Classes
Person
Person::Single < Person
Person::Married < Person
Lets say I have two Persons that are Single => Person::Single
Then i get two Persons when I do:
Person.all.count
=> 2
But my goal is to get only the persons that are type Person and not Person::Single or Person::Married
What I tried is:
Person.where(type: Person.to_s)
=> 2
but this also returns 2 because Person::Single and Person::Married inherit from Person
What can I do instead?
Is there for example an way to say that ::Single and ::Married have the same methods from person but are not the same type? Thanks

Since this is marked with the rails tag, I assume your Person class is an ActiveRecord model. There is some more information missing, but I will just assume where I don't really know ;)
Your Person class inherits the ActiveRecord methods, for example "all", and Person::Single (and ::Maried) will inherit it through Person.
So, naturally, when you do a Person.all it will just do the regular <#ActiveRecord>.all method, which does not know and not care about ::Single and ::Maried.
What you can do is dynamically setting the default_scope, based on the class name of which you are calling .all, or .where or whatever on.
class Person < ActiveRecord::Base
...
def self.default_scope
scope = if self.name == Person.sti_name
where(:type => Person.sti_name)
else
nil
end
scope
end
end
This will also scope your where(:whatever => x) queries.
However, this is asuming you have an attribute called "type" for you Person model (column in database).
EDIT: As Holger pointed out, there is already magic involved with rails STI. I still think it is viable to set a default scope, only you should skip setting the scopes for single and married.
If you don't need it as a default scope, you should just try Person.where(:type => Person.sti_name) or Person.where(:type => Person.name) since rails asserts the class name to sti_name if the class inherits from ActiveRecord::Base.

Try this:
Person.where(:type => Person).count

Related

Is it possible to get Rails associations to return subclasses instead of the superclass?

First, the inheritance approach may not be correct. If so, please explain a different approach.
Here is an example of the setup.
I have many applications, other than using the exact same database, that are not connected. To dry up the code base, I have an engine with all of the active record classes. I would like to keep application specific scopes and methods within the main applications.
In my Rails engine,
class MyEngine::User < ActiveRecord::Base
has_many :dogs
end
class MyEngine::Dog < ActiveRecord::Base
belongs_to :user
end
In my main app,
class User < MyEngine::User
end
class Dog < MyEngine::Dog
# EDITED TO SHOW EXAMPLE OF HOW SCOPE DOESN'T BELONG IN ENGINE
DOGS_WITH_SPOTS_IDS = [ 1, 2, 3, 4 ]
scope :with_spots, -> { where(id: DOGS_WITH_SPOTS_IDS) }
end
The issue in my main app,
user = User.last
user.dogs
# => #<ActiveRecord::Associations::CollectionProxy[#<MyEngine::Dog spots: true>]>
user.dogs.with_spots
NoMethodError: undefined method 'with_spots' for #<MyEngine::Dog::ActiveRecord_Association_CollectionProxy:0x007fa4bf838e50>
Although this works
class User < MyEngine::User
has_many :dogs
end
I would prefer not to redefine/define all of the associations in the main apps.
This type of problem is just going to keep occurring in different forms throughout all of my main applications. This made me think that there may be a way to redefine the associations.
Is there a way to evaluate the association on the subclass instead of the superclass much like the STI pattern to get Rails helpers to recognize different subclasses?
Meaning, I would like my main app User#dogs to return main app Dog instead of MyEngine::Dog.
# may not be the exact code, just off the top of my head
instance_eval do
def model_name
self.class.name
end
end
Important Note You can't query user.dogs.with_spots, even without inheritance. Your probably mean to get array of Dogs in user.dogs.to_a.
Solution The most general way to solve your problem is to use Single Table Inheritance.
Let's see how it works.
I will assume there are already tables my_engine_users and my_engine_dogs defined with all the required columns (as much as MyEngine module is concerned).
Classes MyEngine::User and MyEngine::Dog remain unchanged.
Classes User and Dog just extend respective engine-classes without any additional declarations about relations among them.
What we do, we tell Rails, that the two tables might be used by different classes (via extension). It's easily achievable by adding type column to the tables.
add_column :my_engine_users, :type, :string
and
add_column :my_engine_dogs, :type, :string
Now you can do all the wonderful things with your users and dogs.
dimakura = User.create(username: 'dimakura')
dora = Dog.create(name: 'Dora', owner: dimakura)
fanta = Dog.create(name: 'Fanta', owner: dimakura)
dimakura.dogs.to_a # => array of Dogs, not MyEngine::Dogs
This magic is achieved by explicitly writing class name in type column.
p dimakura #=> #<User id: 1, username: "dimakura", type: "User">
p dora #=> #<Dog id: 1, owner_id: 1, name: "Dora", type: "Dog">
p fanta #=> #<Dog id: 2, owner_id: 1, name: "Fanta", type: "Dog">
MyEngine::User.dogs returns [proxied by an association instance] a set of MyEngine::Dog instances, that in general might have no with_spots scope.
If you are aware that all MyEngine::Dogs “respond” to with_spots, just move the scope definition to MyEngine::Dog. If that is not the case, calling this scope on MyEngine::User.dogs makes no sense.
UPD the example added might be solved:
class MyEngine::Dog < ActiveRecord::Base
belongs_to :user
scope :with_spots, -> { self.class.const_get(:SCOPE_PROC) }
end
class Dog < MyEngine::Dog
DOGS_WITH_SPOTS_IDS = [ 1, 2, 3, 4 ]
SCOPE_PROC = -> { where(name: DOGS_WITH_SPOTS_IDS }
end
I just wanted to post a follow up:
While #dimakura is correct, it still did not work for what I was trying to do. This is because Rails does not allow chaining scopes for STI. In the end, I removed the STI and used the following:
user = User.first
user.dogs.merge(Dog.with_spots)

Sharing a class instance between two classes

I have two different classes that both represent objects that need to be persisted to my database and now I want to share the database client object between the two classes. I want to avoid instantiating the client object more than once.
Currently I do this by using a global variable
$client = Mysql2::Client.new(:database => "myDb", :user => "user", :password => "password", :host => "localhost")
class Person
def save
$client.query("INSERT INTO persons")
end
end
class Car
def save
$client.query("INSERT INTO cars")
end
end
This works, but I am wondering if there are more correct ways to do this and why they are more correct?
You can inherit from a parent class. This allows you to share common functionality across objects and follows DRY (do not repeat yourself) programming principles. It will also allow you to protect your DB connection with locks, resuces, queues, pools, and whatever else you may want to do without having to worry about it in your children classes
class Record
#table_name = nil
##client = Mysql2::Client.new(:database => "myDb", :user => "user", :password => "password", :host => "localhost")
def save
##client.query("INSERT INTO #{#table_name}") if #table_name
end
end
class Person < Record
#table_name = "persons"
end
class Car < Record
#table_name = "cars"
end
While we are on the subject, you should look at using ActiveRecord for handling your database models and connections. It already does pretty much anything you'll need and will be more compatible with other gems already out there. It can be used without rails.
As an alternative on using inheritance, why not consider a simple Singleton pattern? This could make your models cleaner, by separating the responsibility outside your classes. And eliminating the need for inheritance.
The example below illustrates this. Only one, single instance of the DataManager class can exist. So, you'll only instantiate it once - but can use it everywhere:
require 'singleton'
class DataManager
include Singleton
attr_accessor :last_run_query
def initialize()
if #client.nil?
p "Initialize the Mysql client here - note that this'll only be called once..."
end
end
def query(args)
# do your magic here
#last_run_query = args
end
end
Next, calling it using the .instance accessor is a breeze - and will always point to one single instance, like so:
# Fetch, or create a new singleton instance
first = DataManager.instance
first.query('drop table mother')
p first.last_run_query
# Again, fetch or create a new instance
# this'll actually just fetch the first instance from above
second = DataManager.instance
p second.last_run_query
# last line prints: "drop table mother"
For the record, the Singleton pattern can have some downsides and using it frequently results in a never-ending debate on whether you should use it or not. But in my opinion it's a decent alternative to your specific question.

Issue loading classes order EDIT: works, although some odd behavior along the way

I'm working on a project to recreate some of the functionality of ActiveRecord. Here's the portion that isn't working
module Associations
def belongs_to(name, params)
self.class.send(:define_method, :other_class) do |name, params|
(params[:class_name] || name.camelize).constantize
end
self.class.send(:define_method, :other_table_name) do |other_class|
other_class.table_name
end
.
.
.
o_c = other_class(name, params)
#puts this and other (working) values in a query
query = <<-SQL
...
SQL
#sends it off with db.execute(query)...
I'm building towards this testing file:
require 'all_files' #holds SQLClass & others
pets_db_file_name = File.expand_path(File.join(File.dirname(__FILE__), "pets.db"))
DBConnection.open(pets_db_file_name)
#class Person
#end
class Pet < SQLClass
set_table_name("pets")
set_attrs(:id, :name, :owner_id)
belongs_to :person, :class_name => "Person", :primary_key => :id, :foreign_key => :owner_id
end
class Person < SQLClass
set_table_name("people")
set_attrs(:id, :name)
has_many :pets, :foreign_key => :owner_id
end
.
.
.
Without any changes I received
.../active_support/inflector/methods.rb:230:in `block in constantize': uninitialized constant Person (NameError)
Just to make sure that it was an issue with the order of loading the classes in the file I began the file with the empty Person class, which, as predicted gave me
undefined method `table_name' for Person:Class (NoMethodError)
Since this is a learning project I don't want to change the test to make my code work (open all the classes, set all the tables/attributes then reopen them them for belongs_to. But, I'm stuck on how else to proceed.)
EDIT SQLClass:
class SQLClass < AssignmentClass
extend SearchMod
extend Associations
def self.set_table_name(table_name)
#table_name = table_name
end
def self.table_name
#table_name
end
#some more methods for finding rows, and creating new rows in existing tables
And the relevant part of AssignmentClass uses send on attr_accessor to give functionality to set_attrs and makes sure that before you initialize a new instance of a class all the names match what was set using set_attrs.
This highlights an important difference between dynamic, interpreted Ruby (et al) and static, compiled languages like Java/C#/C++. In Java, the compiler runs over all your source files, finds all the class/method definitions, and matches them up with usages. Ruby doesn't work like this -- a class "comes into existence" after executing its class block. Before that, the Ruby interpreter doesn't know anything about it.
In your test file, you define Pet first. Within the definition of Pet, you have belongs_to :person. belongs_to does :person.constantize, attempting to get the class object for Person. But Person doesn't exist yet! Its definition comes later in the test file.
There are a couple ways I can think that you could try to resolve this:
One would be to do what Rails does: define each class in its own file, and make the file names conform to some convention. Override constant_missing, and make it automatically load the file which defines the missing class. This will make load order problems resolve themselves automatically.
Another solution would be to make belongs_to lazy. Rather than looking up the Person class object immediately, it could just record the fact that there is an association between Pet and Person. When someone tries to call pet.person, use a missing_method hook to actually define the method. (Presumably, by that time all the class definitions will have been executed.)
Another way would be do something like:
define_method(belongs_to) do
belongs_to_class = belongs_to.constantize
self.class.send(:define_method, belongs_to) do
# put actual definition here
end
self.send(belongs_to)
end
This code is not tested, it's just to give you an idea! Though it's a pretty mind-bending idea, perhaps. Basically, you define a method which redefines itself the first time it is called. Just like using method_missing, this allows you to delay the class lookup until the first time the method is actually used.
If I can say one more thing: though you say you don't want to "overload" method_missing, I don't think that's as much of a problem as you think. It's just a matter of extracting code into helper methods to keep the definition of method_missing manageable. Maybe something like:
def method_missing(name,*a,&b)
if has_belongs_to_association?(name)
invoke_belongs_to_association(name,a,b)
elsif has_has_many_association?(name)
invoke_has_many_association(name,a,b)
# more...
else
super
end
end
Progress! Inspired by Alex D's suggestion to use method_missing to delay the creation I instead used define_methodto create a method for the name, like so:
define_method, :other_class) do |name, params|
(params[:class_name] || name.camelize).constantize
end
define_method(:other_table_name) do |other_class|
other_class.table_name
end
#etc
define_method(name) do #|params| turns out I didn't need to pass in `params` at all but:
#p "---#{params} (This is line 31: when testing this out I got the strangest error
#.rb:31:in `block in belongs_to': wrong number of arguments (0 for 1) (ArgumentError)
#if anyone can explain this I would be grateful.
#I had declared an #params class instance variable and a getter for it,
#but nothing that should make params require an argument
f_k = foreign_key(name, params)
p f_k
o_c = other_class(name, params)
o_t_n = other_table_name(o_c)
p_k = primary_key(params)
query = <<-SQL
SELECT *
FROM #{o_t_n}
WHERE #{p_k} = ?
SQL
row = DBConnection.execute(query, self.send(f_k))
o_c.parse_all(row)
end

ActiveRecord Scope: Strange behaviour with select on relation

I have two different models with a 1:N relation.
Let's name them 'myobject' and 'related'
class Myobject < ActiveRecord::Base
has_many :related
scope :without_related, includes(:related).select{ |o| o.related.size == 0 }
end
class Related < ActiveRecord::Base
end
The defined scope seems to work great as long as I don't create new assignments from Myobjects to Related:
Direct rails c command "Myobject.includes(:related).select ... (as defined in Scope) works as expected
Calls to scope "Myobject.without_related" still return objects that have been assigned in the meantime
It seems that this can be fixed by restarting the rails console or restarting Webrick.
But I can't always restart a webapplication only because a relation between objects has been changed ;)
Is there any way to fix this problem or to write the scope in a better way?
PS: I need this query as scope to pass its name as group_method to a grouped_select in the form of the Myobject model
Your problem is that in fact your scope is not scope :)
Scopes must return relations, but your scope returns array.
Though it can work as you expect, if you wrap it in lambda
scope :without_related, lambda{ includes(:related).select{ |o| o.related.size == 0 } }
But I recommend to rewrite this code as usual class method to not mislead those who'll work with this code in future
def self.without_related
includes(:related).select{ |o| o.related.size == 0 }
end
or use counter cache, as advised in other answer.
I would recommend you to use counter_cache for this, you need to add column *related_count* of type int to Myobject, make migration and then you will be able to do so:
class Myobject < ActiveRecord::Base
has_many :related
scope :without_related, where(related_count: 0)
end
class Related < ActiveRecord::Base
belongs_to :myobject, counter_cache: true
end
After that you will have super fast scope for getting all objects with no related records and an a count of that objects as well
Or if you know column name that should be present in related table, use this definition:
class Myobject < ActiveRecord::Base
has_many :related
scope :without_related, includes(:related).where('related.id', true)
end

How to magically supply Active Record scopes with arguments?

I'm not sure this is even possible, but let's see if one of you comes up with a solution. This is more or less about code quality in terms of readability and not an actual problem because I already have a solution. I have a friendship model and a user model. The friendship model is used to model friendships between two users:
class Friendship
def self.requested(user)
where(:user_id => user).where(:status => 'requested')
end
def self.pending(user)
where(:user_id => user).where(:status => 'pending')
end
def self.accepted(user)
where(:user_id => user).where(:status => 'accepted')
end
# ...
end
class User
has_many :friendships
# ...
end
Is it somehow possible to call the requested, pending or accepted scope through
the user model without providing an argument?
a_user.friendships.pending # this does not work, is there a way to get it working?
a_user.friendships.pending(a_user) # works of course!
I think this should work if you take the argument off. Calling pending off of the user object like this should already scope friendships to the appropriate user. Define the method like this:
def self.pending
where(:status => 'pending')
end
And call:
a_user.friendships.pending
Check the logs for the generated query if you're not sure it's working.
If you still want to call it by passing an argument I'd name that method Friendship.pending_for(user).

Resources