I am building a search with the keywords cached in a table. Before a user-inputted keyword is looked up in the table, it is normalized. For example, some punctuation like '-' is removed and the casing is standardized. The normalized keyword is then used to find fetch the search results.
I am currently handling the normalization in the controller with a before_filter. I was wondering if there was a way to do this in the model instead. Something conceptually like a "before_find" callback would work although that wouldn't make sense on for an instance level.
You should be using named scopes:
class Whatever < ActiveRecord::Base
named_scope :search, lambda {|*keywords|
{:conditions => {:keyword => normalize_keywords(keywords)}}}
def self.normalize_keywords(keywords)
# Work your magic here
end
end
Using named scopes will allow you to chain with other scopes, and is really the way to go using Rails 3.
You probably don't want to implement this by overriding find. Overriding something like find will probably be a headache down the line.
You could create a class method that does what you need however, something like:
class MyTable < ActiveRecord::Base
def self.find_using_dirty_keywords(*args)
#Cleanup input
#Call to actual find
end
end
If you really want to overload find you can do it this way:
As an example:
class MyTable < ActiveRecord::Base
def self.find(*args)
#work your magic here
super(args,you,want,to,pass)
end
end
For more info on subclassing checkout this link: Ruby Tips
much like the above, you can also use an alias_method_chain.
class YourModel < ActiveRecord::Base
class << self
def find_with_condition_cleansing(*args)
#modify your args
find_without_condition_cleansing(*args)
end
alias_method_chain :find, :condition_cleansing
end
end
Related
I want to override the << setter in my relation. For example, given:
class Library < ActiveRecord::Base
has_many :groups
def readers
groups.find_by(name: 'readers').users
end
end
class Group < ActiveRecord::Base
has_many :group_memberships
has_many :users, through: :group_memberships
end
class GroupMembership < ActiveRecord::Base
belongs_to :user
belongs_to :group
end
class User < ActiveRecord::Base
has_many :groups, through :group_membership
end
I want to do something like
someLibrary.readers << user1
and some additional things to happen after this.
The code should look something like:
def <<(objects)
super objects
#do other things here
end
Where should it be? I guess in Group, like:
class Group
...
def users<<(objects)
super objects
#do stuff
end
end
but I only want to do it when I'm invoking << on readers.
I want to know if there is a way to know whether I'm invoking << on a group users relationship, or whether I have access to group object when I'm invoking << method on group users through the relationship.
I want to do it because it looks nice. The easiest way would be to define separate method to set readers (and be more explicit), but I want to know if it is possible in activerecord or in ruby.
edit:
Yeah I know that overriding core methods is bad thing and people go to hell for that, yada yada yada.
I'm just curious how it's done. Like, for learning purposes.
Besides the aim is just to override the << method on that particular relation so probable there might be some justification why someone might want to do it.
Obligatory disclaimer:
I do not recommend that you do this, in 'important' code. Changing the behaviour of methods like this will confuse the hell out of other developers (as well as your future self), and lead to all sorts of unintended behavioural changes!
But assuming that this is 'just for fun'...
Based on the information above, someLibrary.readers returns a collection of User records. So all we need to do is add the desired behaviour to that class.
Normally you can do this by just defining a class method, in one of two ways:
class User
def self.foo
puts 'this works!'
end
class << self
def bar
puts 'this works too!'
end
end
end
With the above in place, you can call the methods like:
someLibrary.readers.foo
someLibrary.readers.bar
...However, there is some rails black magic going on under the hood here. someLibrary.readers is actually an instance of User::ActiveRecord_Associations_CollectionProxy, and the above methods are being picked up dynamically and appended to ActiveRecord::Associations::CollectionProxy.
Because of this dynamic method definition, it is not possible to override existing Rails methods (such as <<) in this manner. Instead, we'll need to monkey-patch the User::ActiveRecord_Associations_CollectionProxy class directly:
class User
class ActiveRecord_Associations_CollectionProxy
def <<(objects)
super(objects)
# do stuff
end
end
end
If you're looking for a better way of doing this however, I'd recommend using a service object design pattern. You can then encapsulate any more complex/custom logic relating to creating/updating/deleting users, libraries, etc. in a clean and isolated abstraction.
The more established way to do this...
class Library < ActiveRecord::Base
has_many :groups
has_one :reader_group -> {groups.find_by(name: 'readers')}
has_many :readers, through: :reader_group, class_name: 'User', foreign_key: 'user_id'
end
And that's it. You can now do
my_library.readers << another_user
I am building a sinatra web app with a sequel database backend. The primary tasks of this app is collecting status messages from different robots, store them in a database and provide various methods to view them. A common denominator in these messages is, that they provide a WGS84 position in lat/lon.
Now I want to provide various filters for querying messages based on their positions, but I want to write these filters only once, test them only once but re-use them in all model-classes with a lat/lon entry.
To boil it down to a very simple example:
Sequel.migration do
up do
create_table(:auvmessages) do
primary_key :id
Float :lat
Float :lon
String :message
end
create_table(:asvmessages) do
primary_key :id
Float :lat
Float :lon
Integer :chargestate
end
end
end
class Auvessage < Sequel::Model
dataset_module do
def north_of(lat)
self.where{ latitude > lat}
end
end
end
class Asvessage < Sequel::Model
dataset_module do
def north_of(lat)
self.where{ latitude > lat}
end
end
end
In both model classes have north_of(lat) to filter for messages which originate north of a given latitude. This function is fairly simple and you can easily repeat it two or three times, but what about more complex cases?
I have played around a bit with modules outside of dataset_module but nothing seem to be right.
Is there a preferred way how to re-use filters over different models? I have searched a lot, but didn't find any satisfying answer.
Edit:
To make my question a bit more precise: I want to move all functions like north_of(lat) (there are a lot more) into a service class. What I want to know now, is the best way to integrate that service class into a sequel-model:
"Just" include it?
Extend dataset_module, and if so, how?
Writing a dataset-plugin?
...
You can pass an existing module to dataset_module:
module NorthOf
def north_of(lat)
where{latitude > lat}
end
end
Auvessage.dataset_module NorthOf
Asvessage.dataset_module NorthOf
As a followup: I have taken #jeremy-evans answer and extended it by a parametrisation scheme for modules. So from now on I can test my filters by mocking and my model classes have just a list of includes in their dataset_module.
I like it.
As explanation my slightly modified example:
Sequel.migration do
up do
create_table(:auvmessages) do
primary_key :id
Float :lat
Float :lon
String :message
end
create_table(:asvmessages) do
primary_key :id
Float :gps_lat
Float :gps_lon
Integer :chargestate
end
end
end
module GPSFilter
def self.create(lat_name, lon_name)
Module.new do
include GPS
define_method :lat_col_name do
lat_name
end
define_method :lon_col_name do
lon_name
end
end
end
def north_of(lat)
where( "#{lat_col_name} > #{lat}" )
end
##### default parameters #####
def lon_col_name
"lon"
end
def lat_col_name
"lat"
end
end
class Auvmessage < Sequel::Model
dataset_module do
include GPSFilter
end
end
class Asvmessage < Sequel::Model
dataset_module do
include GPSFilter.create :gps_lat, :gps_lon
end
end
Here is a link to Uncle Bob's Screaming Architecture blog post which might be of help.
Now, answering your question, it seems that north_of, as well as many other methods, are actually part of your domain logic. This logic should not go in persistence abstractions, or controllers, or views, etc.
Design, build and write tests for the set of objects that solves your problem in the language of the domain of your problem. Then, you'll have at hand a rich set of functionality that you can simply use on Models, Controllers, CLIs, etc.
I usually put my service objects in a lib/ directory and write simple unit tests, without any of the persistence boilerplate that sets up test databases. They usually run very fast as well.
I started off with https://gist.github.com/scttnlsn/1295485 as a basis to make a restful sinatra app. I'm having difficulty, though, managing HaBTM relationships for paths such as
delete '/:objecttype/:objid/:habtm_type/:habtm_id'
I already have the objecttype thanks to the map (as per that gist), and pulling the right object from the db with the id is straightfoward. However, getting the other side of the habtm and calling the appropriate method on objecttype to delete the relationship involves turning a handful of strings into the appropriate objects and methods.
I came up with a solution, but it uses eval. I'm aware that using eval is evil and doing so will rot my very soul. Is there a better way to handle this, or should I put in some safeguards to protect the code and call it a day?
Here's a working, self contained, sinatra-free example to show how I'm doing the eval:
require 'mongoid'
require 'pp'
def go
seed
frank = Person.find_by(name:"Frank")
apt = Appointment.find_by(name:"Arbor day")
pp frank
really_a_sinatra_route(frank.id, "appointments", apt.id)
frank.reload
pp frank
end
def really_a_sinatra_route(id, rel_type,rel_id)
# I use "model" in the actual app, but hardwired a person here to
# make a simpler example
person = Person.find_by(id: id)
person.deassociate(rel_type,rel_id)
end
class Base
def deassociate(relationship,did)
objname = associations[relationship].class_name
# Here's the real question... this scares me as dangerous. Is there
# a safer way to do this?
obj = eval "#{objname}.find(did)"
eval "#{relationship}.delete(obj)"
end
end
class Person < Base
include Mongoid::Document
has_and_belongs_to_many :appointments
end
class Appointment < Base
include Mongoid::Document
has_and_belongs_to_many :persons
end
def seed
Mongoid.configure do |config|
config.connect_to("test_habtmexample")
end
Mongoid.purge!
frank=Person.create(name:"Frank")
joe=Person.create(name:"Joe")
ccon = Appointment.create(name:"Comicon")
aday = Appointment.create(name:"Arbor day")
frank.appointments << ccon
frank.appointments << aday
ccon.persons << joe
joe.reload
end
go
A nice gentleman on freenode helped me out. Those two evals can be replaced with:
obj= self.send(relationship.to_sym).find(did)
self.send(relationship.to_sym).delete(obj)
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
I had the following function. It worked, but I don't like the way it looked.
# in user.rb
def awarded_requests
Request.joins('JOIN application ON application.request_id = request.id').where('accepted = true AND application.user_id = ?', self.id)
end
Then I refactored it to something that's clearly an improvement, but probably not simplest possible form:
def awarded_requests
Request.find(self.applications.accepted.map(&:request_id))
end
Can this be simplified further?
If you set up has many relationship, you can filter out those requests by merging a scope.
class User
has_many :applications
def awarded_requests
Request.joins(:applications).merge(applications.accepted)
end
end
Note that applications.accepted is not an array of records but a scope. This is how Active Record represents a part of SQL query internally, therefore it can smartly combine a few of them.