How to serialize relational models as JSON in Ruby on Rails? - ruby-on-rails

Say I have a basic User model:
# Name | Type | Attributes
# ----------------------------- | ------------------ | ---------------------------
# **`id`** | `bigint` | `not null, primary key`
# **`email`** | `string` | `default(""), not null`
# **`encrypted_password`** | `string` | `default(""), not null`
A user can have many companies they manage. So user.rb contains:
has_many :company_permissions
has_many :companies, -> { distinct }, through: :company_permissions
Company is pretty simple:
# Name | Type | Attributes
# ------------------ | ------------------ | ---------------------------
# **`id`** | `bigint` | `not null, primary key`
# **`description`** | `text` |
# **`name`** | `string` |
User and Company are related by a Rails relational model called CompanyPermission:
# Name | Type | Attributes
# ----------------- | ------------------ | ---------------------------
# **`id`** | `bigint` | `not null, primary key`
# **`name`** | `string` |
# **`company_id`** | `bigint` | `not null`
# **`user_id`** | `bigint` | `not null`
A permission's name can be read, write, or delete. If a user has more than 1 permission (e.g. read and write) then there are 2 relational models.
A user can see a JSON response of their current companies:
class CredentialsController < AuthorizedController
def me
render json: current_user, include: %i[companies]
end
end
This produces output like:
{
"id": 676,
"email": "email1#example.com",
"companies": [{
"id": 786,
"name": "Some Company",
"description": "Some Company Description"
}]
}
As you can see, the user doesn't know which permissions they have. I'd like to include the information from CustomerPermission.name here, perhaps something like:
{
"id": 676,
"email": "email1#example.com",
"companies": [{
"id": 786,
"name": "Some Company",
"description": "Some Company Description",
"permissions": ["read", "write"] # <------- I want this
}]
}
Do I need to write an ActiveModel::Serializer from scratch for this? Or is there some other way to easily include this detail from the relational model in the JSON output?

You can do this using github.com/Netflix/fast_jsonapi as Alex mentioned in the comment above.
Keep in mind, this does change the structure to adhere to Google's JSON:API standard. Everything will be nested within the "attributes" property. And when you use include method, it won't include all the data from, for example, Companies - just the id.
Essentially, you need to create a custom method and violate the standard in order to achieve that, but it made total sense in my situations to do that opposed to making multiple API calls.
Create a serializer for your User and Company models, and within that you can define a custom attribute block like:
# app/serializers/user_serializer.rb
class UserSerializer
include FastJsonapi::ObjectSerializer
attribute :companies do |user|
CompanySerializer.new(user.companies).serializable_hash[:data]
end
end
# app/serializers/company_serializer.rb
class CompanySerializer
include FastJsonapi::ObjectSerializer
attribute :permissions do |company|
company.company_permissions.map{|perm| perm.name}
end
end
Hope that helps :)

Related

ActiveRecord: how to build has_many relation with foreign key or null

I have these two models:
class Promotion < ActiveRecord::Base
belongs_to :product
end
class Product < ActiveRecord::Base
has_many :promotions
end
And consider these 3 promotions:
--------------------------
| PROMOTIONS |
--------------------------
| id | name | product_id |
| 1 | sale | NULL |
| 2 | 10% | 1 |
| 3 | 20% | 2 |
--------------------------
When the product_id is NULL, the promotion applies to all products, so I'd like to be able to get all the promotions for a product as follows:
Product.find(1).promotions # => [1,2]
Product.find(2).promotions # => [1,3]
How can I achieve this?
You could go about finding promotions like this a few different ways. One way would be to just access the Promotion model directly
promotions1 = Promotion.where(product_id: [1, nil])
promotions2 = Promotion.where(product_id: [2, nil])
You could add this as a method in your Product model
class Product < ActiveRecord::Base
def all_promotions
Promotion.where(product_id: [self.id, nil])
end
end
And then use it as follows:
Product.find(1).all_promotions # => [1,2]
Another way could be to concat the promotions of Product object with all promotions that aren't attached to a specific product. This definitely isn't the best solution, since you can't really take advantage of ActiveRecord to order the promotions; you'd have to use array sorting.
def all_promotions
self.promotions + Promotion.where(product_id: nil)
end

How to using "where" to dynamic build realation

Sry for broken english
I have 2 table fruits and berries and 2 model fruit and berry, both id are primary key, berries's id is a foreign key of fruits.
The meaning is if fruit's attr is "berry" then this fruit will have hp, atk, def. other just a normal fruit, they don't have hp, atk, def.
i'm tring "where" but not work, and i have no idea to add foreign key to migrate file
it's any solutions can solve this realation
fruits
+-----+------------+-----------+
| id | name | attr |
+-----+------------+-----------+
| 123 | Blueberry | berry |
| 932 | Apple | not berry |
| 429 | Banana | not berry |
| 563 | Strawberry | berry |
+-----+------------+-----------+
berries
+-----+----+-----+-----+
| id | hp | atk | def |
+-----+----+-----+-----+
| 123 | 15 | 5 | 5 |
| 563 | 7 | 10 | 3 |
+-----+----+-----+-----+
Fruit
class Fruit < ActiveRecord::Base
has_one :berry, -> { where attr: "berry"}, foreign_key: 'id'
end
Berry
class Berry < ActiveRecord::Base
belongs_to :fruit
end
First of all bannanas are considered berries... sometimes
There are at least 2 ways of doing this
Single Table Inheritance (STI)
Multiple Table Inheritance
In STI you only create the fruits table in the database, but add all the columns the Berry class will need. Even if this method will leave many blank spaces in the DB where fruits aren't berries, I recommend it because it is pretty straight forward and supported by rails. To use it change your attr column to type and add the hp, atk and def columns in a migration:
rails g migration AddAttrsToFruit hp:integer atk:integer def:integer
rails g migration ChangeAttrToType
Since the migration generator doesn't do magic like when the migration starts with the word Change as it does with Add, you have to edit the change function in the migration it creates to look like this:
rename_column :fruits, :attr, :type
Then change your Berry class to inherit from Fruit instead of ActiveRecord::Base
class Berry < ActiveRecord::Base
belongs_to :fruit
end
Now when you create a Berry
Berry.create(name: 'Coconut', hp:100, atk:5, def:999)
Rails creates a the record in the Fruit table with all the attributes filed in:
#<ActiveRecord::Relation [#<Berry id: 1, name: nil, type: "Berry", created_at: "2015-10-14 02:38:09", updated_at: "2015-10-14 02:38:09", hp: 1, atk: nil, def: nil>]>
For MTI you can read the link.
Good luck :)
Great answer from robertoplancarte - to explain a little more simply for you, you're looking to use a has_many/belongs_to relationship:
#app/models/fruit.rb
class Fruit < ActiveRecord::Base
has_many :berries
end
#app/models/berry.rb
class Berry < ActiveRecord::Base
belongs_to :fruit
end
You can set it up in your database as follows:
#fruits
+-----+------------+-----------+
| id | name | attr |
+-----+------------+-----------+
| 123 | Blueberry | berry |
| 932 | Apple | not berry |
| 429 | Banana | not berry |
| 563 | Strawberry | berry |
+-----+------------+-----------+
#berries
+-----+----------+----+-----+-----+
| id | fruit_id | hp | atk | def |
+-----+----------+----+-----+-----+
| 1 | 123 | 15 | 5 | 5 |
| 2 | 932 | 10 | 3 | x |
+-----+----+-----+----+-----+-----+
This will allow you to call...
#fruit = Fruit.find params[:id]
#fruit.berries
What robertoplancarte was saying was your current setup is pretty weak:
You're identifying which "fruit" is a berry manually
You're then populating another model with data which could be put into the first
The way around this is to use something called an STI - Single Table Inheritance.
This is a Railsy way to use a single model to define multiple types of data:
#app/models/fruit.rb
class Fruit < ActiveRecord::Base
#columns id | type | name | hp | atk | def | created_at | updated_at
end
#app/models/berry.rb
class Berry < Fruit
end
This will give you the ability to call:
#berry = Berry.find x
This is more appropriate for your requirements; is somewhat advanced, but nothing a question on StackOverflow would be defeated by.

Rendering a JSON object of a join-model and its associated models

In a Rails ( 4.1.5 / ruby 2.0.0p481 / win64 ) application I have a many-to-many relationship between Student and Course and a join model StudentCourse which represents the association, and has an additional attribute called started (set by default on "false").
I also have added an index in the join-table made of student_id and course_id, and set a unique check on that, like this
t.index [:student_id, :course_id], :unique => true, :name => 'by_student_and_course'
I wanted that to be a composite primary key, but since in rails there are no composite primary keys (without using a gem) I also added a primary key called id:
t.column :id, :primary_key
Now I see that associations are created by either doing:
Student.first.courses.create(:name => "english")
or
Course.first.students << Student.first
This is fine and it's the expected behaviour, I suppose.
That said, I am struggling to wrap my mind around association resolutions in ActiveRecord queries. Let me explain this better:
For reference, Student.all, Course.all and StudentCourses.all would return tables like these:
Student.all
+----+-----------+
| id | name |
+----+-----------+
| 1 | Aidan |
| 2 | Alison |
| 3 | Elizabeth |
+----+-----------+
Course.all
+----+----------+------------------+
| id | name | description |
+----+----------+------------------+
| 1 | english | desc. here |
| 2 | music | desc. here |
| 3 | dance | desc. here |
| 4 | science | desc. here |
| 5 | french | desc. here |
| 6 | arts | desc. here |
+----+----------+------------------+
StudentCourse.all
+-------------+------------+------------+----+
| course_id | student_id | started | id |
+-------------+------------+------------+----+
| 1 | 1 | false | 1 |
| 2 | 1 | false | 2 |
| 3 | 1 | false | 3 |
| 1 | 2 | true | 4 |
| 2 | 2 | true | 5 |
| 4 | 3 | false | 6 |
| 5 | 2 | false | 7 |
| 5 | 1 | true | 8 |
| 6 | 2 | false | 9 |
+-------------+------------+------------+----+
So far I can happily render a json object of all courses, and names of all students for each course like this:
render json: Course.all.to_json(:include => {:students => {:only => :name}})
I can also easily render all courses that a student is attending or about to attend with
render json: #student.courses.to_json(:include => {:students => {:only => :name}})
which also includes other students for those courses.
But suppose I wanted to render one student's courses which the student has not yet started, together with all the other students who are on that course (which have or not started the course) [Please read the UPDATE section below, I'm looking for the opposite thing actually!]
I think the easieast approach is to query the join-table for something like:
StudentCourse.all.where(student_id: #student.id, started: false)
+-------------+------------+------------+----+
| course_id | student_id | started | id |
+-------------+------------+------------+----+
| 5 | 2 | false | 7 |
| 6 | 2 | false | 9 |
+-------------+------------+------------+----+
But how do I go on from this resulting table (association object) to get a nicely packaged json object with courses names (and all other attributes) and also including students in it, like I did with: Course.all.to_json(:include => {:students => {:only => :name}}) ?
I think I'm missing some basic knowledge of some important key concepts here, but at this point I cannot even indentify them and would greatly appreciate some help... thank you.
Update:
I just realized that the following part is what I was originally trying to do. It's the opposite thing. Among all these details I got lost along the path. I hope that it's ok if I just add it here.
So, given a student (let's call him Aiden), I need to return a JSON object containing only the courses that he is in and that he has started, only when such courses have other students in them who have not started them, and it has to include the names of those students for each course too.
So...
I now have:
aiden_started_courses = Student(1).courses.where(:student_courses => {:started => true } )
which for a student takes all the courses that have a "true" value in the join-table "started" column. (again in the join table each student-course record is "compositely" unique, so there can just be one unique record for a given student_id and course_id).
With the next query, for one of "aiden_started_courses" I can pull off all the relative student-courses associations which have a false value on "started"
aiden_started_courses[0].student_courses.where(:started => false).includes(:student).to_json(:include => :student)
+-------------+------------+------------+----+
| course_id | student_id | started | id |
+-------------+------------+------------+----+
| 1 | 2 | false | 4 |
+-------------+------------+------------+----+
| 1 | 9 | false | 5 |
+-------------+------------+------------+----+
So here lies the problem: I have managed to get this just for a single course in aiden_started_courses array, but how would I be able to build a query that returns this data for all of Aiden's started courses?
Is it possible to do that in one line? I know I could probably use Ruby enumerator loops but I somewhat feel that I would be kind of breaking some pattern both on a Rails coding convention level and on performance level? (hitting N+1 problem again?) ...
What I could so far:
I came up with this where I find all students who have not started the courses which a given user has started:
Student.includes(:student_courses).
where(:student_courses => { :started => false, :course_id => aiden.courses.where
(:student_courses => {started: true}).ids } )
or this:
Course.includes(:students).where(:student_courses => {:started => false,
:course_id => aiden.courses.where(:student_courses => {:started =>true}).ids })
which finds all the courses that a given student has started if those courses include students who have not started them yet
But what I really need is to get a JSON object like this:
[
{
"id": 1,
"name": "english",
"students": [
{"name": "ALison"},
{"name": "Robert"}]
},
{
"id": 2,
"name": "music",
"description": null,
"students": [
{"name": "Robert"},
{"name": "Kate"}]
}
]
where I can see the courses that a given student is on and has started, but only those in which there are other students that have not yet started it, together with the names of those students...
I'm thinking that probably there is no way how I could get that through a regular AR query, so maybe should a build a JSON manually? But how could I do that?
Thanks in adv. and I apologise for the verbosity.. but hopefully it will help..
Use scope to your advantage:
class Course < ActiveRecord::Base
# ...
scope :not_started, -> { joins(:student_courses) \
.where(student_courses: {started: false}) }
scope :with_classmates, -> { includes(:students) } # use eager loading
end
Then call:
#student.courses.not_started.with_classmates \
.to_json(include: {students: {only: :name}})
Output:
[
{
"id": 1,
"name": "english",
"description": null,
"students": [
{"name": "Aiden"},
{"name": "Alison"}]},
{
"id": 2,
"name": "music",
"description": null,
"students": [
{"name": "Aiden"},
{"name": "Alison"}]},
{
"id": 3,
"name": "dance",
"description": null,
"students": [
{"name": "Aiden"}]}]
Use JBuilder, it comes by default with Rails. Ignore the lines starting with '#':
Jbuilder.new do |j|
# courses: [{
j.courses <student.courses - replace this with whatever variable> do |course|
# id: <course.id>
j.id course.id
# name: <course.name>
j.name course.name
# students: [{
j.students <course.students - replace with whatever variable> do |student|
# name: <student.name>
j.name student.name
end
# }]
end
# }]
end
Not a lot of code. Removing the comments and simplifying some features, it will look like:
student_courses = <...blah...>
json = Jbuilder.new do |j|
j.courses student_courses do |course|
j.(course, :id, :name)
j.students <course.students - whatever method here>, :name
end
end.target!
Check out their guide, its pretty awesome to generate JSON in plain Ruby DSL. So go ahead and use whatever ruby code you want to fetch students/courses/studentcourses.

Counting entries on association table and inject it into a model, using Rails

The goal
Count entries on association table and inject it into a model, using Rails (v. 4.1).
The scenario
There is game.rb model:
class Game < ActiveRecord::Base
has_and_belongs_to_many :genres
end
and there is also a genre.rb model:
class Genre < ActiveRecord::Base
end
In the database, there are three tables: games, genres, games_genres – and games is still empty because I'm still developing genres area. So, genres' table is like following:
+----+-----------+
| id | name |
+----+-----------+
| 1 | Action |
| 2 | RPG |
+----+-----------+
And this is games_genres table:
+----+--- -----+----------+
| id | game_id | genre_id |
+----+---------+----------+
| 1 | 1 | 1 |
| 2 | 1 | 2 |
| 3 | 2 | 1 |
+----+---------+----------+
The problem
My application has an API and to retreive genres, I'm doing this way:
class API::V1::TargetsController < ApplicationController
def index
render json: Genre.all.to_json
end
end
The output is something like this:
[
{
id: 1,
name: 'Action'
},
{
id: 2,
name: 'RPG'
}
]
But, I want to inject the count of how many products has each genre. The query is simple:
SELECT COUNT(genre_id) AS products_quantity FROM game_genres
So, how can I inject products_quantity see above's query within Genre model? Something to get the JSON's output like this:
[
{
id: 1,
name: 'Action',
products_quantity: 2
},
{
id: 2,
name: 'RPG',
products_quantity: 1
}
]
You can add a method option to the to_json method. Thus you can define a product_quantity method on your Genre model
def product_quantity
game_genres.count
end
and then have it included in the to_json call.
class API::V1::TargetsController < ApplicationController
def index
render json: Genre.all.to_json(methods: :product_quantity)
end
end
While the above will work, I would suggest you use something more robust like rabl to handle JSON responses.

Sharded Tables Combined into a Single ActiveRecord Model in Rails

I am new to Ruby on Rails (which may soon be obvious) and I'm trying to figure out a model for the following data scenario. I have read several posts and searched Google at length, but I'm still confused.
I have 5 different tables with identical columns, except the value column has a different data type. They data is in 5 separate tables for a variety of good reasons, but think of it as data sharded across multiple tables.
logbook_strings (user_id, entry_id, field_id, value)
logbook_booleans (user_id, entry_id, field_id, value)
logbook_integers (user_id, entry_id, field_id, value)
logbook_decimals (user_id, entry_id, field_id, value)
logbook_datetimes (user_id, entry_id, field_id, value)
So here's what the data would look like:
------------------------------------------------
| user_id | entry_id | field_id | value |
------------------------------------------------
| 1 | alpha1 | date | 2012-11-14 |
| 1 | alpha1 | duration | 1.2 |
| 1 | alpha1 | remarks | Nice job. |
------------------------------------------------
| 1 | alpha2 | date | 2012-11-13 |
| 1 | alpha2 | duration | 2.7 |
| 1 | alpha2 | remarks | Bad job. |
------------------------------------------------
Entry alpha1:
2012-11-14, 1.2, Nice Job.
Entry alpha2:
2012-11-13, 2.7, Bad job.
etc.
The reason I do this is so that I can have an infinitely flexible database. I can add a new field_id at any time to add a new field/feature to my app instead of doing a schema update to add yet another column to a wide logbook table.
So what I'm wondering, is there a way I can have a single ActiveRecord model in which I can reference all 5 of these tables?
After spending a few minutes trying to shoehorn this into a single ActiveRecord class, I don't think it's a great idea to use ActiveRecord for something like this. I see a few options:
Roll your own model. The extreme downside to this approach is that you lose out on all of ActiveRecord's many nice features. But if your data is relatively simple (not a lot of associations, etc.) then this might be a viable option.
Restructure your data. If this schema/data is either pre-existing or has to match a mobile app's schema for some reason or another, this might not be an option. But if you're starting fresh, Rails' migrations make adding/removing columns on a whim extremely easy and very safe so I might consider using a more traditional approach. While this may not seem ideal, it's something to seriously consider in order to gain the many benefits of ActiveRecord.
If you must keep your schema, creating a separate model for each logbook table could be your best option.
# Migrations
create_table :logbook do |t|
# Default fields, nothing special
end
create_table :logbook_integers do |t|
t.integer :logbook_id # You'd probably want to index this as well
t.string :name
t.integer :value
end
create_table :logbook_strings do |t|
t.integer :logbook_id # You'd probably want to index this as well
t.string :name
t.string :value
end
# etc...
# Models
class Logbook < ActiveRecord::Base
has_many :logbook_integers
has_many :logbook_strings
# etc...
def remarks
self.logbook_strings.find_by_name("remarks").value
end
def remarks= newValue
remark = self.logbook_strings.find_or_create_by_name("remarks")
remark.value = newValue
remark.save
end
# etc...
end
class LogbookInteger < ActiveRecord::Base
belongs_to :logbook
end
class LogbookString < ActiveRecord::Base
belongs_to :logbook
end
# etc...
# Usage
logbook = Logbook.new
logbook.remarks = "Hi"
logbook.duration = 2
logbook.remarks # => Hi
logbook.duration # => 2
If you can change your schema a bit, here's an option:
You can use the serialize class method described here (cmd+f for 'serialize') to store your entries so instead of having many models, you just have two: Logbook and LogbookField. It might look something like this:
# Migration for logbook_fields
create_table :logbook_fields do |t|
t.string :name
t.string :value
end
# Models
class Logbook
has_many :logbook_fields
def self.build_with_default_fields
self.logbook_fields.create name: "date"
self.logbook_fields.create name: "duration"
# etc...
end
# You could probably do some cool Ruby metaprogramming to create all these
# accessors/setters for you, btw.
def date
self.logbook_fields.find_by_name "date"
end
def date= newValue
field = self.logbook_fields.find_by_name "date"
field.value = newValue
field.save
end
def duration
self.logbook_fields.find_by_name "duration"
end
def duration= newValue
field = self.logbook_fields.find_by_name "duration"
field.value = newValue
field.save
end
# etc...
end
class LogbookField
serialize :value
belongs_to :logbook
end
# Usage
logbook = Logbook.build_with_default_fields
logbook.date = DateTime.now
logbook.duration = 2.7
Something to that effect. That way you retain most all the ActiveRecord niceties while still maintaining some of the "infinite-ness" of your schema design. However, adding/removing columns on a single table with migrations would probably prove easier than this even. Again, it depends on whether you can be flexible in your schema or not. Hope this helps.
I think you should probably have a one table with a type column.
Ex:
logbook(user_id, entry_id, field_id, value, value_type)
value type would be
strings
booleans
integers
decimals
datetimes
Example would be
-----------------------------------------------------------
| user_id | entry_id | field_id | value |value_type |
-----------------------------------------------------------
| 1 | alpha1 | date | 2012-11-14 | datetime
| 1 | alpha1 | duration | 1.2 | decimal
| 1 | alpha1 | remarks | Nice job. | string
So basically value column will be string, and from the model you could decide what you wanted to with the value type, your model would be
class Logbook < ActiveRecord::Base
#sample method
#just to give an idea how you could use the same value
#with different times
def multiple_duration_by_two
self.value * 2 if self.value_type == "decimal"
end
end
However, depending on your requirements this implementation might need tweeks, but I guess you get the idea
HTH

Resources