How to model differing criteria? - ruby-on-rails

I need to be able to match prospective owners to abandoned animals based on varying criteria and locations.
The owner will have a particular criteria set. Animal type = "dog", breed = "Labrador Retriever", age will need to be between 1 and 5, sex = male, and so on...
The animal will also have a particular criteria set. The animal type = "Dog", age = 3, sex = male, breed = "Chihuahua".
The animal could also be: type = "Cat", age = "12", sex = female, breed = "Tiger".
I also have a "Location" model for both the owner and the animal (polymorphic) that contains the information related to the location of either the animal or the owner.
So that part is easy...
The hard part (at least for me) is when I need to specify different criteria for different animal types. So an animal of type = "dog" may have a criteria of "can fetch?" whereas an animal of type cat may have a criteria of "de-clawed?" and a animal of type "fish" may have criteria of "pattern" with multiple options of ["speckled", "striped", "plain"].
What I have now is an "animal" model with the generic animal information (age, sex, breed), then I have a breeds model with the various breeds per animal type, but I can't figure out how to abstract out the criteria that differs between the animal types.
Again, this is just an analogy because I don't think my actual problem will make any sense to anyone else. What I need is just some pointers in the right direction, maybe a link or two. I just can't seem to work out how to make this happen in Rails without creating a separate table for each criteria set, as in dog_criteria, cat_criteria, fish_criteria, and so on...

Sometimes we resort to simple name/value pairs (or name/value/type triples ) for such properties. This saves adding new types (and database tables) for every animal species in the world. Or worse for each breed: Consider poodles ... they might have an extra field "shaved decoratively" ... and St Bernards, brandy capacity ...

In the software modeling world, this would obviously be done with a class for each species, I.E. "Dog", "Cat", "Fish" which extend "Animal". In the relational database world, this becomes a bit harder to represent.
If you wanted to match this object-oriented approach in your database, you would have a table for "Animals" and then a table for each species, "Cat", "Dog", and "Fish". Then you would probably have a species table (or a hard-coded enum in your code) that would give you a value to place in the Animal row for which species each animal was. This would tell you how to look up further information for each animal.
This is probably not the best approach. What you have is more what I would call "Custom Data" for each animal. You should define one table that has a list of custom attributes, and another table to match these attributes to a value for each animal row.
If you'd like to make it more convenient to see and control which attributes can apply to which species you could make a third table for "Categories" which would link to the Animal species and to a collection of attributes. Then you would specify the category ID on the animal row.
Sample tables:
Animals
-------
ID
Age
Sex
Species
Breed
Parameters
----------
ID
Name
Parameter Values
----------------
ParameterID
AnimalID
Value
Categories (optional - add CategoryID to animals)
---------------------
ID
Name
Category Parameters
-------------------
CategoryID
ParameterID

The name value pair solution can be done within the database.
In this example there are 4 tables, person, requirement, pet, petstat
Person and Pet each have an id and a name.
Each person also has one or more requirements. Requirements have a name, comparison, and a value.
Each pet has one or more PetStats which consist of a name and a value
with this setup, the short haired dogs could be found with a query like :
select * from pet, petstat
where pet.id = petstat.petId
and petstat.name = 'hair'
and petstat.value = 'short'
and petId in (select petId from petstat
where name='type' and value = 'dog')
The following query will match people to pets when all of the person's requirements are met by a pet.
select person.name, pet.name from person, pet
where (select count(*) from requirement where requirement.personid = person.id)
= (select count(*) from requirement, petstat
where requirement.comparison = 'eq'
and requirement.name = petstat.name
and requirement.value = petstat.value
and requirement.personId = person.id
and petstat.petid = pet.id)
+ (select count(*) from requirement, petstat
where requirement.comparison = 'lt'
and requirement.name = petstat.name
and requirement.value > petstat.value
and requirement.personId = person.id
and petstat.petid = pet.id)
+ (select count(*) from requirement, petstat
where requirement.comparison = 'gt'
and requirement.name = petstat.name
and requirement.value < petstat.value
and requirement.personId = person.id
and petstat.petid = pet.id);
The less than and greater than comparisons could be handled better by adding numeric fields to the requirement and petstat tables and adding more pieces to the query, but this gives a good stat.
Here are the inserts to create the test data.
delete from person;
insert into person (id, name) values (1, 'Joe');
insert into person (id, name) values (2, 'Bill');
insert into person (id, name) values (3, 'Erik');
insert into person (id, name) values (4, 'Mike');
delete from pet;
insert into pet (id, name) values (1, 'spot');
insert into pet (id, name) values (2, 'mittens');
insert into pet (id, name) values (3, 'rover');
delete from requirement;
insert into requirement (personid, name, comparison, value) values (1, 'type', 'eq', 'dog');
insert into requirement (personid, name, comparison, value) values (1, 'color', 'eq', 'black');
insert into requirement (personid, name, comparison, value) values (2, 'type', 'eq', 'fish');
insert into requirement (personid, name, comparison, value) values (3, 'type', 'eq', 'dog');
insert into requirement (personid, name, comparison, value) values (3, 'hair', 'eq', 'long');
insert into requirement (personid, name, comparison, value) values (4, 'type', 'eq', 'dog');
insert into requirement (personid, name, comparison, value) values (4, 'weight', 'lt', '30');
insert into requirement (personid, name, comparison, value) values (4, 'weight', 'gt', '20');
delete from petstat;
insert into petstat (petId, name, value) values (1, 'type', 'dog');
insert into petstat (petId, name, value) values (1, 'color', 'black');
insert into petstat (petId, name, value) values (1, 'hair', 'short');
insert into petstat (petId, name, value) values (2, 'type', 'cat');
insert into petstat (petId, name, value) values (3, 'type', 'dog');
insert into petstat (petId, name, value) values (3, 'weight', '25');
insert into petstat (petId, name, value) values (3, 'color', 'brown');

Related

Select objects from different models and union them rails

I have to activerecord models
Model1 and Model2
Model1 has name and info fields
Model2 has state and price
I want to make an array of objects which will be a union of model1 + model2.
Each sum_model object should have name, info, state, price properties.
It should be the sum of Model1.all and Model2.all. For example:
If Model1.all returns one record with name="name" and info ="zz"
Model2.all returns one record with state='state' and price=14
model_sum should be an array of 2 objects:
[{name:'name', info: 'zz', state: '', price: ''}, {name:'', info: '', state: 'state', price: 14}]
How do I do this?
You should be able to do what you want to achieve by building the cartesian product (combine every record of one table with every table of another table) between Model1 and Model2 and then get the attributes of the resulting model (which will have the attributes of Model1 and Model2 combined:
Model1
.joins("CROSS JOIN #{Model2.table_name}")
.select("#{Model1.table_name}.*, #{Model2.table_name}.*")
.map(&:attributes)
No association needs to exist between the two models in order to do that.
Edit after rereading the question and noticing that I missed the point
You will first have to adjust the selects of both models in order to get the UNION to match columns. Luckily, one can add arbitrary selects into the AR statement, so e.g.
m1 = Model1.select(:name, :info, "'' as state", "'' as price")
m2 = Model2.select(:state, :price, "'' as name", "'' as info")
Then you will have to handcraft the UNION as this is not supported by AR. It is by Arel but I most of the time doubt that Arel is worth the struggle:
union = Model1.connection.unprepared_statement do
"((#{m1.to_sql}) UNION (#{m2.to_sql})) AS unioned"
end
And after that you can fire away the select using the union as the from value and then call the attributes method of each returned entry to achieve the result you specified:
Model1
.select('*')
.from(union)
.map(&:attributes)

Rails/PostgreSQL : query value inside an integer array column

What I wrong with me or this query?
I have a Shop model with an opening days column, which is an array of integers (something you can do with Postgres):
days= [1,1,1,1,0,0,0]
When I query:
shops = Shop.where('days[0] = 1')
I get an empty ActiveRecord Relation.
=> #<ActiveRecord::Relation []>
When I take a shop with this kind of array…
shop = Shop.first
=> #<Shop id: 215, days: [1, 1, 1, 1, 0, 0, 0],…
If I do
shop.days[0]
I get
=> 1
I really don't get it.
By default PostgreSQL uses a one-based numbering convention for arrays, that is, an array of n elements starts with array[1] and ends with array[n].
— Source
It's just your example. Your index is out of bounds, so it doesn't match any records, days[0] is NULL. Everywhere. Fire up rails db and figure:
SELECT * FROM shops WHERE days[0] IS NULL;
But what's with that "by default"? Is it possible to define array bounds on schema level so this never becomes an issue?
Well... it's Rails' fault, I'm afraid. If it's even considered an "issue" at all. In order for an array to be zero-indexed it should be saved as such in SQL. I tried that in SQL, it works:
INSERT INTO shops
(days, created_at, updated_at)
values('[0:2]={1, 1, 0}', current_timestamp, current_timestamp);
Unfortunately, Rails loses bounds for some reason:
Shop.create(days: '[0:3]={6, 7, 8, 9}')
> INSERT ... [["days", "{6,7,8,9}"], ...]

Activerecord: Getting a type error when using alias for a table

I have a table with items, and a table with item characteristics.
Corresponding models:
class Characteristic < ActiveRecord::Base
belongs_to :item
end
class Item < ActiveRecord::Base
has_many :characteristics
end
Each characteristic has it's name (i.e. 'price'), value, an a reference to an item.
I need to select items by multiple characteristics, say, price = 100 and weight = 50. For this, i need to join tables twice, like this:
Item.joins('INNER JOIN characteristics c1 ON c1.item_id = items.id').
joins('INNER JOIN characteristics c2 ON c2.item_id = items.id').
where('c1' => {name: 'price', value: '100'},
'c2' => {name: 'weight', value: '50'})
and this is where the problem is. Characteristic's value is stored in database as string, and when i try to compare it to an integer, or range, I get a type conversion error. But when i don't use an alias for a table, there is no error.
So, the code below works:
Item.joins('INNER JOIN "characteristics" ON "characteristics"."item_id" = "items"."id"').
where(characteristics: {characteristic_type_id: 223, value: 380})
But this one does not:
Item.joins('INNER JOIN "characteristics" c1 ON c1."item_id" = "items"."id"').
where(c1: {characteristic_type_id: 223, value: 380})
So how can I select items with, say, price in 50..100 and color 'brown'?
UPD:
neither of above code works, actually. First one does not produce an SQL error, but it does the wrong thing. It just quotes the value so it becomes string. I.e.
where(c1: {value: 10..15})
becomes
WHERE ("c1"."value" BETWEEN '10' AND '15')
which is, obviously, not what I really want
So I decided do add one more field to characteristics, value_f:decimal{8,2} to hold the numeric value of characteristic. I also added
after_validation do
self.value_f = value.to_f
end
to characteristic's model. So, when I want to compare a value to a number, I just use value_f instead.
Try this:
Item.joins(:characteristics).where(characteristics: [{name: 'price', value: '100'},{name: 'weight', value: '50'}] )
Found the solution:
Postgresql. CREATE CAST 'character varying' to 'integer'
CREATE CAST (varchar AS integer) WITH INOUT AS IMPLICIT;
Still have to figure out how to execute this on Heroku, but that's a different question

Produce a report by getting data from multiple tables

I have 4 tables:
key: id, name
project: id, name
project_key: id, key_id, project_id
project_report: id, status, project_id, key_id
project_c_report: id, status, project_id, key_id, c_id
I want to produce a report using those tables:
The output should be:
Key.name, project_report.status, project_c_report.status
I was able to do this by getting all the keys from a project, and loop over them
array = []
project.keys.each do |k|
p = ProjectReport.where(keyword_id: k, project_id: p.id).map(&:status)
c = ProjectCReport.where(keyword_id: k, project_id: p.id, c_id:1).map(&:status)
array << {name: k.name, pr: p, pcr: c}
end
array
The problem is that I am doing a lot of selects and everything is slow, can someone help me please with a better way of doing this.
Thank you
First, create a function in your DataBase. This is just a brief example, and also its done in PostgreSQL but shouldnt difer much
from MySQL, SQLServer, etc
Function get_myreport(key_id integer, project_id integer [As many params as you'd like the function to get))
pProject ALIAS FOR $1;
pKey ALIAS FOR $2;
BEGIN
CREATE TEMP TABLE IF NOT EXISTS tmp_project_report(id integer, project_name character varying, *All the values you want to see in the report);
TRUNCATE tmp_project_report;
INSERT INTO tmp_project_report(all the columns)
SELECT a.table1_fields, b.table2_fields, c.table3_fields, d.table4_fields, e.table5_fields
FROM table1 a, table2 b, table3 c, table4 d, table5 e
WHERE
a.key = pKey
AND b.project_key = pProject
END;
Then, in your controller's method you call the up the function like this
myFunction = ActiveRecord:Base.connection.execute = "Select get_myreport("param1, param2, etc...")
You will have to make a model where you put all the fields that are on the temp_table you've made, and also you will set the temp_table as the self.table_name
Then, in your view, you'd only have to iterate on your collection and display the values accordingly
#report = TempTable.all
<% #report.each_do |report| %>
<% report.value1 %>
<% etc... %>
<% end %>
Figure out the database query, then query the database directly from your model:
def records
connection = ActiveRecord::Base.connection
records = connection.select %Q {
SELECT key.name, project_report.status, project_c_report.status
FROM ...
JOIN ...
;
}
records
end
Here is something you can try if you choose to keep this within Rails (note that the following query is untested and is shown for concept only):
report_data = Project.joins(project_key: :key)
.joins('left join project_reports on project_keys.project_id = project_reports.project_id and project_keys.key_id = project_reports.key_id
left join project_c_reports on project_keys.project_id = project_c_reports.project_id and project_keys.key_id = project_c_reports.key_id')
.where('project_c_reports.c_id = ?', 1)
.select('projects.name, project_reports.status as report_status, project_c_reports.status as c_report_status')
This should give you an array of Project objects each including the selected three attributes name, report_status, c_report_status. To get these values in an array of these three elements you could do:
report_data.map { |p| [ p.name, p.report_status, p.c_report_status ] }
The type of join for the query depends on your requirement. Given the index are in place the query should be better compared to how it looks in code!

How to copy a (Active)record between tables, partially?

In two tables mapped to ActiveRecord with unknown number of identical columns, e.g.:
Table A Table B
--------- ---------
id id
name name
age email
email is_member
How can I (elegantly) copy all identical attributes from a record of Table A to a record of Table B, except the id attribute?
For the example tables above, name and email fields should be copied.
Try this:
Get intersection of the columns between TableA and TableB
columns = (TableA.column_names & TableB.column_names) - ["id"]
Now iterate through TableA rows and create the TableB rows.
TableB.create( TableA.all(:select => columns.join(",") ).map(&:attributes) )
Edit: Copying one record:
table_a_record = TableA.first(:select => columns.join(","), :conditions => [...])
TableB.create( table_a_record.attributes)
Migt consider using a union function on the acitverecord attributes hash between the 2 tables. It's not a complete answer but may help

Resources