Is it possible to re-use sunspot searchable declarations from other models? - ruby-on-rails

I'm using sunspot to do fulltext searches in a customized spree webshop project.
There are Products, which are already extensively indexed:
class Product
searchable do
text :description
... lots of other declarations
end
end
I now need to index Orders, which (via some other models, but I don't think this is important), have_many Products:
class Order
has_many :products
end
The question is: I want Orders to be searchable via their products, using the same indexed attributes. I want to stay DRY and not add variations of the declarations on Product to a new searchable block on Order, but rather say something like "orders are searchable via products". Is this possible somehow ? The docs for sunspot don't mention anything like this.
EDIT:
"apneadiving" suggested putting the declarations in some sort of shared module and re-using it that way. I might be mistaken, but I think this can't work because the sunspot DSL refers to the current model, so the declarations from Product would not work on Order.
For example, if I could apply the searchable block from the example above in Order, it would instruct sunspot to index Order on description, which it doesn't have, and doesn't help me with indexing by products anyway.

Related

Active Record join for class in module

Rails 6/ Ruby 2.7
So I have two classes ModuleX::SubModuleA::Order and ModuleY::SubModuleB::OrderType
and I want to do something like
ModuleX::SubModuleA::Order.joins("ModuleY::SubModuleB::OrderType")...
This syntax would be pretty simple normally: ModuleX::SubModuleA::Order.joins(:order_type) but I cannot find any documentation regarding how this works for classes in modules.
The syntax of joins allows two uses: first, referencing a relation defined on the left-hand model by its (symbolic) name, or second, raw SQL. Neither method is actually impacted at all by the use of modules, because neither references the class of the model you're joining onto.
In your case, you probably want to set up a relation between orders and order types, like:
class ModuleX::SubModuleA::Order
belongs_to :order_type, class_name: 'ModuleY::SubModuleB::OrderType'
end
Then, you can just do the same syntax as you expect:
ModuleX::SubModuleA::Order.joins(:order_type).all

How do I avoid using Concerns when designing Rails database schemas?

I read many blogs, and one of the themes that comes across often is that concerns (at least the way Rails defines them) are damaging to software. On balance I agree - simply including behaviour into models is violating the single responsibility principle. You end up with a god-class that does too much.
But as with many of the opinions gleaned from blogs, an alternative architecture is rarely provided.
So let's take an example app, loosely based on one I have to maintain. It's inherently a CMS, as many Rails apps tend to be.
Currently each model has a large number of concerns. Let's use a few here:
class Article < ActiveRecord::Base
include Concerns::Commentable
include Concerns::Flaggable
include Concerns::Publishable
include Concerns::Sluggable
...
end
You can imagine that 'Commentable' would require only a small amount of code added to the Article. Enough to establish relationships with comment objects and provide some utility methods to access them.
Flaggable, allowing users to flag inappropriate content, ends up adding some fields to the model: flagged, flagged_by, flagged_at for example. And some code to add functionality.
Sluggable adds a slug field for referencing in URLs. And some more code.
Publishable adds publish date and status fields, with yet more code.
Now what happens if we add a new kind of content?
class Album < ActiveRecord::Base
include Concerns::Flaggable
include Concerns::Sluggable
include Concerns::Publishable
...
end
Albums are a bit different. You can't comment on them, but you can still publish them and flag them.
Then I add some more content types: Events and Profiles, let's say.
I see a few problems with this architecture as it stands:
We have multiple database tables with exactly the same fields
(flagged_by, published_on etc.)
We can't retrieve multiple content types at once with a single SQL query.
Each model supports the duplicated field names with the included concerns, giving each class multiple responsibilities.
So what's a better way?
I've seen decorators promoted as a way to add functionality at the point where it's needed. This could help solve the issue of included code, but the database structure isn't necessarily improved. It also looks needlessly fiddly and involves adding extra loops to the code to decorate arrays of models.
So far my thinking goes like this:
Create a common 'content' model (and table):
class Content < ActiveRecord::Base
end
The associated table is probably quite small. It should probably have some kind of 'type' field, and maybe some things common to absolutely all content - like a type slug for URLs perhaps.
Then rather than adding concerns we can create an associated model for each behaviour:
class Slug < ActiveRecord::Base
belongs_to :content
...
end
class Flag < ActiveRecord::Base
belongs_to :content
...
end
class Publishing < ActiveRecord::Base
belongs_to :content
...
end
class Album < ActiveRecord::Base
belongs_to :content
...
end
...
Each of these is associated with one piece of content, so the foreign key can exist on the feature's model. All the behaviour relating to the feature can also exist solely on the feature's model, making OO purists happier.
In order to achieve the kind of behaviour that usually requires model hooks (before_create for example) I can see an observer pattern being more useful. (A slug is created once a 'content_created' event is sent, etc.)
This looks like it would clean things up no end. I can now search all content with a single query, I don't have duplicated field names in the database and I don't need to include code into the content model.
Before I merrily unleash it on my next project, has anyone tried this approach? Would it work? Or would splitting things up this much end up creating a hell of SQL queries, joins and tangled code? Can you suggest a better alternative?
Concerns are basically just a thin wrapper around the mixin pattern. It is a very useful pattern for composing pieces of software around reusable traits.
Single Table Inheritance
The issue of having the same columns across several models is often solved with Single Table Inheritance. STI however is only really suited when the models are very similar.
So lets consider your CMS example. We have several different types of content:
Page, NewsArticle, BlogPost, Gallery
Which have pretty much identical database fields:
id
title
content
timestamps
published_at
published_by
# ...
So we decide to get rid of duplication and use a common table. It would be tempting to call it contents but that is extremely ambiguous - content of the content ...
So lets copy Drupal and call our common type Node.
class Node < ActiveRecord::Base
include Concerns::Publishable
end
But we want to have different logic for each type of content. So we make subclasses for each type:
class Node < ActiveRecord::Base
self.inheritance_column = :type
include Concerns::Publishable
end
class NewsArticle < Node
has_and_belongs_to_many :agencies
end
class Gallery < Node
has_and_belongs_to_many :photos
end
# ...
This works well until the STI models start to diverge for too much from each other. Then some duplication in the database schema can be a far smaller problem than the massive complications caused by trying to shoehorn everything into the same table. Most CMS systems built on relational databases struggle with this issue. One solution is to use a schemaless non-relational database.
Composing concerns
There is nothing in that says that concerns require you to store on the models table. Lets look at several of the concerns you have listed:
Flaggable
Sluggable
Commentable
Each of these would use a table flags, slugs, comments. The key is making the relation to object that they flag, slug or comment polymorphic.
comment:
commented_type: string
commented_id: int
slugs:
slugged_type: string
slugged_id: int
flags:
flagged_type: string
flagged_id: int
# ...
class Comment
belongs_to: :commented, polymorphic: true
end
module Concerns::Commentable
# ...
has_many: :comments
end
I would suggest that you look at some of the libraries that solve these kind of common tasks such as FriendlyId, ActsAsTaggedOn etc to see how they are structured.
Conclusion
There is nothing fundamentally wrong with the idea of parallel inheritance. And the idea that you should forgo it just to placate some kind extreme OO purity ideal is ridiculous.
Traits are a part of object orientation just any other composition technique. Concerns are however not the magic-fix all that many blog posts would have you believe.

Filtering a Mongoid collection through relations

I'm using Mongoid to build a Rails 4 app.
The problem I'm having right now is how to filter some Mongoid objects through their own relations and have a Mongoid::Criteria at the end instead of an Array.
This is some example code:
class Editor
include Mongoid::Document
has_many :books
def likes
books.collect { |book| book.likes }
end
end
class Book
include Mongoid::Document
belongs_to :editor
end
What I would like to be able to do is something like:
Editor.last.likes.where(:created_at.gt => 10.days.ago)
but of course this doesn't work as Editor.last.likes returns an Array, not a Mongoid::Criteria
I know Mongoid has an aggregation framework but it's not entirely clear to me how to use it, nor if it's the best way to solve my problem.
Suggestions?
TIA,
ngw
The biggest problem you have here is that MongoDB does not do joins like a relational database does. All of the work you are getting for convenience when traversing object properties is being done client side while pulling in the 'related' documents over the wire in a query. But in sending a query, the two collections cannot be joined.
Your workaround solution is to work with the data you can get at in separate queries in order to target the results. One approach is here: rails mongoid criteria find by association
There should be other examples on Stack Overflow. You're not the first to ask.

Querying a collection with mongoid and ruby

I want to query many different collections from a model in rails.
For example:
class Statistics
include Mongoid::Document
end
I want to be able to query a statistics or maybe a my_stats collection. Is that possible? How can I do it?
Your question isn't clear. If you want to know how to define your own collection name, instead of having Mongoid auto-generate one via the class name/ActiveSupport, you can do that using the store_in method. Like so:
class Statistics
include Mongoid::Document
store_in :mystats
end
If you're asking how to search multiple collections with one query, that isn't possible in MongoDB, as far as I'm aware.

Mongo Design question with Rails/Mongoid for a bill tracking app

I'm writing a quick app for a user to track their daily bills (for money tracking purposes). I want the user to be able to define their own categories that a bill can be applicable for. I'm trying however to decide the best way to model this and also validate categories as unique.
My initial thought was this:
class User
include Mongoid::Document
embeds_many :bills
field :categories, :type => Array
end
class Bill
include Mongoid::Document
embeded_in :user, :inverse_of => :bills
field :category
index :category
end
So a user can add categories, just as strings, and when they add a bill, they'll choose from their available categories for the bill.
So, a couple questions:
Does this seem like the proper design? I Don't think it's necessary to define an actual category model as it's literally just a string used to index bills on, but I'm not sure if there are other benefits to a separate model
How do I validate_uniqueness_of :categories in my user model. I don't think it works on array items like this, but I could be wrong. I don't want a user to create categories with the same name. I suppose this might be the advantage of a separate model, embedded in the User, but again it seems like more work than necessary.
Can someone tell me my best options here to validate that a user has unique categories (but users can have the same categories, i obviously don't care about that, just unique in the scope of a single user)
[Update]
The design seems proper. In a Rails specific way how would you validate the uniqueness? When adding a category pull the list and do an indexOf check to ensure it doesn't exist. If it does just bounce back an error.
I'm not a Rails guy, let me know if I'm off track or something.
I'm not sure MongoDB would be the best choice of storage engines for that. You would be better off using MySQL with a categories table.
Knocks against MongoDB:
Not ACID transactions
No single server durability
Not relational (you want relational for a bill tracking application)

Resources