Rails Mongoid includes doesn't load child models - ruby-on-rails

I don't know exactly should it works how i expect or not, but i think that this:
def show
render json: Book.includes(:genres).find(params[:id])
end
Should include genres in book model.
class Book
include Mongoid::Document
...
has_and_belongs_to_many :genres
end
class Genre
include Mongoid::Document
field :name, type: String
end
But on client in contains only list of genre ids.
genre_ids: Array[1]
0: Object
$oid: "53532d3b616c6439c1070000"
I also tried another model Author with book:
class Author
include Mongoid::Document
field :name, type: String
has_many :books, inverse_of: :author
end
# in controller action
render json: Book.includes(:author).find(params[:id])
# returns =>
# {"book":{"_id":{"$oid":"53532d3b616c6439c1140000"},"annotation":null,"author_id":{"$oid":"53532d3b616c6439c1000000"},"author_name":"Сомерсет Моэм","co_author_ids":[],"created_at":"2014-04-20T02:13:16.057Z","date":"1947","genre_ids":[{"$oid":"53532d3b616c6439c1070000"}],"html_path":"/Users/alder/Projects/pro_book_reader/rails-api/storage/GoCUMSZP/_.html","image_url":"GoCUMSZP.jpg","name":"Театр","toc_path":null,"token":"h9beplTN"}}
Same thing, it returns only $oid (author_name is a book model property)
So, could i load all genres models with book model, without extra queries?
Rails 4.1, mongoid 4.0.0.beta1
Also i don't have association in Genre model, because it will save all book ids in genre model.
UPDATED
Identity Map has been removed from mongoid 4. There is new preload_models option, but it's not load genres too when true.
PS
It works with render json: {book: book, genres: book.genres}, so maybe thats fine until includes will be fixed.
PS2
I think perhaps this includes is not what i'm thinking of. I mean maybe it's just helps to avoid extra queries after you appeal to relational model(s). And it doesn't create array from all childs, or add author object to author field (that what any expect, not useless ID).

To display genres for each of all available books:
def show
books = Book.all
render json: books, include: :genres
end
To display genres for a particular book:
def show
book = Book.find(params[:id])
render json: book, include: :genres
end

You have to enable the identity map in mongoid.yml for eager loading to work
identity_map_enabled: true
in options.

Identity Map has been removed from mongoid master (mongoid4). You can check with the issue opened for further details, https://github.com/mongoid/mongoid/issues/3406.
Also mongoid 4 change log shows in Major Changes (Backward Incompatible) as identity map is removed. https://github.com/mongoid/mongoid/blob/006063727efe08c7fc5f5c93ef60be23327af422/CHANGELOG.md.
As they suggested, Eager load will work without identity map now.

Related

Rails Has Many Validate Unique Attribute With Accepts Nested Attributes For

I have Invoices with many Invoice Line Items. Invoice line items point to a specific item. When creating or updating an Invoice, I'd like to validate that there is not more than 1 invoice line item with the same Item (Item ID). I am using accepts nested attributes and nested forms.
I know about validates_uniqueness_of item_id: {scope: invoice_id}
However, I cannot for the life of me get it to work properly. Here is my code:
Invoice Line Item
belongs_to :item
validates_uniqueness_of :item_id, scope: :invoice_id
Invoice
has_many :invoice_line_items, dependent: :destroy
accepts_nested_attributes_for :invoice_line_items, allow_destroy: true
Invoice Controller
// strong params
params.require(:invoice).permit(
:id,
:description,
:company_id,
invoice_line_items_attributes: [
:id,
:invoice_id,
:item_id,
:quantity,
:_destroy
]
)
// ...
// create action
def create
#invoice = Invoice.new(invoice_params)
respond_to do |format|
if #invoice.save
format.html { redirect_to #invoice }
else
format.html { render action: 'new' }
end
end
end
The controller code is pretty standard (what rails scaffold creates).
UPDATE - NOTE that after more diagnosing, I find that on create it always lets me create multiple line items with the same item when first creating an invoice and when editing an invoice without modifying the line items, but NOT when editing an invoice and trying to add another line item with the same item or modifying an attribute of one of the line items. It seems to be something I'm not understanding with how rails handles nested validations.
UPDATE 2 If I add validates_associated :invoice_line_items, it only resolves the problem when editing an already created invoice without modifying attributes. It seems to force validation check regardless of what was modified. It presents an issues when using _destroy, however.
UPDATE 3 Added controller code.
Question - how can I validate an attribute on a models has many records using nested form and accepts nested attributes?
I know this isn't directly answering your qestion, but I would do things a bit differently.
The only reason InvoiceLineItem exists is to associate one Invoice to many Items.
Instead of having a bunch of database records for InvoiceLineItem, I would consider a field (e.g. HSTORE or JSONB) that stores the Items directly to the Invoice:
> #invoice.item_hash
> { #item1: #quantity1, #item2: #quantity2, #item3: #quantity3, ... }
Using the :item_id as a key in the hash will prevent duplicate values by default.
A simple implementation is to use ActiveRecord::Store which involves using a text field and letting Rails handle serialization of the data.
Rails also supports JSON and JSONB and Hstore data types in Postgresql and JSON in MySQL 5.7+
Lookups will be faster as you don't need to traverse through InvoiceLineItem to get between Invoice and Item. And there are lots of great resources about interacting with JSONB columns.
# invoice.rb
...
def items
Item.find( item_hash.keys)
end
It's a bit less intuitive to get "invoices that reference this item", but still very possible (and fast):
# item.rb
...
# using a Postgres JSON query operator:
# jsonb ? text → boolean (Does the text string exist as a top-level key or array element within the JSON value?)
# https://www.postgresql.org/docs/current/functions-json.html#FUNCTIONS-JSONB-OP-TABLE
def invoices
Invoice.where('item_hash ? :key', key: id)
end
After reading this through a few times I think see it as:
An invoice has many invoice_line_items
An invoice has many items through invoice_line_items
Item_id needs to be unique to every invoice, thus no repeating of items in the line_item list.
Items are a catalogue of things that can show up in multiple invoices. i.e. items are things like widget_one and widget_two, and an invoice can only contain one line with widget one, but many invoices could contain this same item. If this is not true and an item will only ever show up in one invoice, let me know and I will change my code.
So I think your validation should not be in Items, as items know nothing about invoices. You want to make sure your join table has no entries where a given invoice_id has duplicate item_id entries.
item.rb:
has_many :invoice_line_items
has_many :invoices, through: :invoice_line_items
invoice.rb:
has_many :invoice_line_items
has_many :items, through: :invoice_line_items
invoice_line_item.rb:
belongs_to :item
belongs_to :invoice
validates_uniqueness_of :item_id, :scope => :invoice_id
This SHOULD give you what you need. When you create a new invoice and save it, Rails should try to save each of the line items and the join table. When it hits the duplicate item_id it should fail and throw an error.
If you are going to have unique constraints in your code I would alway back it up with a constraint in your database so that if you do something that makes an end run around this code it will still fail. So a migration should be added to do something like:
add_uniq_constraint_to_invoice_line_items.rb:
def change
add_index :invoice_line_items, [:invoice_id, :item_id], unique: true
end
This index will prevent creation of a record with those two columns the same.

Rails 6 API What are good practices for sending and serializing data?

I have problem and I don't know how to solve it in a correct way. In my front-end app I have select which shows all products, so I need to send request to my Rails API. Controller has method index that sends all products, but with many different attributes and associations. I don't think so it's a good idea to send request to this method, because in <select> I need only product name and id. Example:
ProductController.rb
def index
render json: #products, include: 'categories, user.company'
end
ProductSerializer.rb
class ProductSerializer < ActiveModel::Serializer
attributes :id, :name, :desc, :weight, :amount, ...
belongs_to :user
has_many :categories
end
As u can see ProductSerializer send many things and it is expected but in a different view in FE app. In another page I need only id and name attributes to <select>. I know that I can create new Serializer and add if like this:
def index
render json: #product, each_serializer: ProductSelectSerializer and return if pramas[:select]
render json: #products, include: 'categories, user.company'
end
But I'm not sure is a good idea to create new Serializer only for one request, because in bigger application can be many situations like this. This if in index method dose not looks good too in my opinion, so maybe I should create new method for this request, but it's worth for one small request? Are there any good practices that can help to properly resolve such situations?
I suggest you to try blueprinter. It a gem that help you to serializing your data and this gem is suitable with your needs.
To create the Blueprinter's serializer you can run this command in your terminal :
rails g blueprinter:blueprint Product
After You create the searializer, you may define different outputs by utilizing views:
class ProductBlueprint < Blueprinter::Base
identifier :id
view :normal do
field :product_name
end
view :extended do
fields :product_name, :product_price
association :user, blueprint: UserBlueprint
association :categories, blueprint: CategoryBlueprint
# this will take the association from your product's model and make sure you have created the CategoryBlueprint and UserBlueprint
end
end
After you define the view, now you can use the view in your controller. In your index action, you can call it with this syntax.
def index
render json: ProductBlueprint.render_as_hash(#product, view: :normal) and return if params[:select]
render json: ProductBlueprint.render_as_hash(#products, view: :extended)
end

Add an array to an ActiveRecord object during api call

I have two classes called classrooms and students. They have a many-to-many relationship because classrooms can have many students, but also students can belong to many classrooms. When I make an api call to get a partular classroom, I would also like to grab all the students belonging to that classroom (through the StudentClassroom join table) and add them to the Classroom ActiveRecord object that I am returing from the api.
If I pause execution with a binding.pry I before returning the response in the controller, #Classroom.students will return an array of student records as expected. However, when #Classroom gets sent to the front end it gets stripped of the students array. What am I doing wrong?
Classroom class
class Classroom < ApplicationRecord
has_many :student_classrooms
has_many :students, through: :student_classrooms
def get_classroom_students(classroom_id)
StudentClassroom.where(classroom_id: classroom_id)
end
end
Controller
def show
students = #Classroom.get_classroom_students(#Classroom.id)
# somehow add students to #Classroom ActiveRecord object
render json: #Classroom
end
This would never return students to the front end. When you add binding.pry and test that on the controller you still have the ActiveRecord object which has the students method defined but when you pass it to the front end you convert it to JSON:
render json: #Classroom
So that JSON doesn't have the student array. To fix this you could try this on the show action:
def show
render json: #Classroom.as_json(include: students)
end
This will include students in the JSON you are passing to the front end.
In case you want to make this behavior default, i.e., whenever the Classroom object is converted to JSON then include the students you can add as_json method on Classroom class like this:
def as_json(options = {})
super({ include: :students }.merge(options))
end
Now, whenever the Classroom object is converted to JSON it would include the students. Examples:
# this will automatically include the students in your controller action
render json: #Classroom
# this will also have the students included
Classroom.last.to_json
When you call render no magic happens: before being sent in the response body your classroom object gets serialized.
Default json serialization for the AR models is roughly speaking a hash of attributes - no associations, no instance methods etc. You can workaround this behavior via redefining as_json for the model as another answer suggests, but this is generally a bad idea - you might (and most probably will) need different serialization for different endpoints and/or scenarios, so having just one hardcoded in the model will force you to add more and more hacking.
The way better idea is to decouple serialization from the business logic - this is where serializers come into play. Take a look at https://github.com/jsonapi-serializer/jsonapi-serializer for example - this is what you need to solve the task in a clean and maintainable way.
Just define a serializer
class ClassroomSerializer
include JSONAPI::Serializer
attributes <attributes you would like to expose>
has_many :students
end
(just a draft - refer to the documentation to unleash all the power :)) and you're good to go with something as simple as just:
def show
render json: ClassroomSerializer.new(#classroom).serializable_hash
end

Mongoid Query Related Collection

I'm trying to figure out how to query related collections in Mongoid.
Here is my object model (simplified for brevity):
Category
-------------------
class Category
include Mongoid::Document
field :name, type: String
...
belongs_to: product
end
class Product
include Mongoid::Document
field :name, type: String
...
has_one :category
end
I'm trying to build a query to get all products with category with a particular name e.g. "Toy"
products = Product.where('category.name' => 'Toy')
I get a nil collection back. I think mongoid does not support this types of queries directly. How do I build a query that will accomplish that?
Try the following
category = Category.find_by(name: 'Toy')
# all the following will work
products = Product.where(category: category)
products = Product.where(category_id: category.id)
this will work, but it's not recommended to use mongoid this way
Mongoid provides relational-style associations as a convenience for application developers who are used to dealing with relational databases, but we do not recommend you use these extensively.

How does one intercept nested_attributes for further processing?

tl;dr: Is it possible to intercept posted values from a nested model for further processing? I've tried everything I can think of to access the nested attributes to use for a before_save callback, but that may be only a testament to the limits of my imagination.
I'm writing a library application, where books can have many authors and vice versa. That bit is working just fine with a has_many_through and accepts_nested_attributes_for. The app saves all the book and author information just fine. Except... I can't seem to write a working before_save on either the Author or Book model to check if the Author that we're trying to create exists. Here's what I already have:
class Book < ActiveRecord::Base
has_many :authorships
has_many :authors, :through => :authorships
accepts_nested_attributes_for :authors, :authorships
end
class Author < ActiveRecord::Base
has_many :authorships
has_many :books, :through => :authorships
before_save :determine_distinct_author
def determine_distinct_author
Author.find_or_create_by_author_last( #author )
end
end
## books_controller.rb ##
def new
#book = Book.new
#book.authors.build
respond_to do |format|
format.html
format.xml { render :xml => #book }
end
end
def create
#book = Book.new(params[:book])
#author = #book.authors #I know this line is wrong. This is the problem (I think.)
# more here, obviously, but nothing out of ordinary
end
When I post the form, the dev log passes on this:
Parameters: {"commit"=>"Create Book",\
"authenticity_token"=>"/K4/xATm7eGq/fOmrQHKyYQSKxL9zlVM8aqZrSbLNC4=",\
"utf8"=>"✓", "book"{"title"=>"Test", "ISBN"=>"", "genre_id"=>"1", "notes"=>"", \
"authors_attributes"=>{"0"{"author_last"=>"McKinney", "author_first"=>"Jack"}}, \
"publisher"=>"", "pages"=>"", "language_id"=>"1", "location_id"=>"1"}}
So... the data's there. But how do I get to it to process it? When I post the data, here's that log:
Author Load (0.4ms) SELECT `authors`.* FROM `authors` WHERE\
`authors`.`author_last` IS NULL LIMIT 1
I've done a fair bit of searching around to see what there is out there on nested attributes, and I see a lot on the form side, but not much on how to get to the assets once they're submitted.
Any explanation of why the solution which works does actually work would also be appreciated. Thanks.
First, this line
#author = #book.authors
assigns an array of authors to #author, but in your before_save you seem to be expecting a single author model. Secondly, the #author variable is scoped to the controller and will be empty in your model's scope.
Remove that line and try this in your Book class (not tested):
def determine_distinct_author
authors.each do |author|
Author.find_or_create_by_author_last( author )
end
end
1) The nested attributes is an array, and should be accessed as an array. Getting the submission from the one model to the next still presents quite a problem.
2) accepts_nested_attributes_for is probably not going to help out a whole lot, and there will undoubtedly be a lot of custom processing which will have to happen between now and when the whole system is fully functional. Probably best to ditch it now and move on.
In that direction: It looks like Ryan Bates has done a recent bit on the jQuery TokenInput plugin which will take care of a lot of the autocomplete feature on the author. More to the larger problem (and a bit harder to find), is a follow up that he posted to some issues with the plugin on working with new entries.
I am not quite sure what you are trying to accomplish, but inside your Author you have a before_save callback, and that is obviously giving the error.
To me it seems you are looking for an author with the given name, and then want to use that? Is that correct?
Using a nested form, you will always create a new author, unless you want to use something like a select-box, and then select an author-id (which is part of the book, so not nested), instead of the authors details.
UX-wise, i would offer the option to either select an existing author (use an autocomplete field), or create a new one (using the nested fields option --without your before_save callback).

Resources