I've got a portfolio model with following fields:
name: string (required)
status: string (required) one of: draft, active, funded
One of the requirement is that a newly created portfolio should have a status draft. I could set a default value inside of migration something like:
create_table :portfolios do |t|
t.string :name, null: false
t.string :status, null: false, default: 'draft'
t.timestamps
end
But I don't think it will easy to maintain. Of course I could set this status inside create method like:
Portfolio.create!(
name: params[:name],
status: 'draft'
)
Is there a better way to create such record? maybe some method inside of model?
class Portfolio < ApplicationRecord
after_initialize do
self.name = "draft"
end
end
I think it's better to do it using after_initialize because this callback will guarantee that the default value will be there from the very beginning of the life cycle of the object
Portfolio.new.name
#shoudl give you draft
Related
So, I'm building an app where I have a backend written in Rails and a client written in Vue with Amplify. My database is MySQL and I'm using AWS AppSync with a GraphQL as data source (pointing to my database).
The AWS Amplify has a framework that allows me to generate the schemas based on the table names and columns with one simple command: amplify api add-graphql-datasource. But because I'm using rails migrations, my database is using Rails conventions: pluralized tables with snake cased columns.
Now, the problem with that is the GraphQL schemas are all ugly and not using the correct conventions (singular names for the types and inputs, with camel cased props). Example:
My backend has the following migration:
class CreatePosts < ActiveRecord::Migration[6.0]
def change
create_table :posts do |t|
t.belongs_to :site, null: false
t.string :title
t.string :url
t.text :body
t.timestamps
end
end
end
And the schema generated for this is:
type posts {
id: Int!
site_id: Int!
title: String
url: String
body: String
created_at: AWSDateTime!
updated_at: AWSDateTime!
}
type Query {
getPosts(id: Int!): posts
listPostss: [posts]
// ...
}
schema {
query: Query
// ...
}
Not to mention this:
input CreatepostsInput {
id: Int!
site_id: Int!
title: String
url: String
body: String
created_at: AWSDateTime!
updated_at: AWSDateTime!
}
So, AWS Amplify is new, it's not mature as Rails, and on top of that I didn't find any adapter or transformer to handle the problem in the client... my hope is to find a way to handle it on Rails.
I need to be able to completely change the Rails conventions without breaking anything: migrations, associations, how to manage associations (create_xxx, build_xxx).
This app is really new, so I can recreate all the migrations from scratch.
Thanks
I can see some things you can do:
Tables:
In your migration, you can change the table name, but now you need to let your model know what's the table name with self.table_name.
# migration
class CreatePosts < ActiveRecord::Migration[6.0]
def change
create_table :post do |t| # singular 'post'
...
end
end
end
#model
class Post
self.table_name = "post" # i think you can also use a symbol :post
end
Attributes:
You need to avoid using Rails migration methods that follow Rails conventions like t.belongs_to or t.references or t.timestamps.
# migration
class CreatePosts < ActiveRecord::Migration[6.0]
def change
create_table :post do |t|
# do not use this below
# t.belongs_to :site, null: false
t.bigint :siteId, null: false
t.string :title
t.string :url
t.text :body
# do not use this below
# t.timestamps
t.datetime :createdAt, default: ->{'CURRENT_TIMESTAMP'}
t.datetime :updatedAt, default: ->{'CURRENT_TIMESTAMP'}
end
end
end
Relationships:
You also need to update your relationships in your models
class Post
belongs_to :site, foreign_key: 'siteId'
end
More information can be found in the Rails API. Make sure you check the documenation for other relationship methods.
Timestamps:
Since timestamps columns (created_at, updated_at) are not the ones expected by ActiveRecord anymore, you might need to override the ActiveRecord::Timestamp module to have them continue working as you would expect. One of the easiest option is to update your ApplicationRecord or a single model class with the following:
class ApplicationRecord # or class Post
before_create :set_timestamps
before_save :set_timestamps
private
def set_timestamps
self.createdAt = DateTime.current if self.new_record?
self.updatedAt = DateTime.now
end
end
Or this other option taken from https://stackoverflow.com/a/52276865/1845602
class ApplicationRecord < ActiveRecord::Base
self.abstract_class = true
class << self
private
def timestamp_attributes_for_create
super << 'my_created_at_column'
end
def timestamp_attributes_for_update
super << 'my_updated_at_column'
end
end
end
Auto generated Inputs:
I might be wrong here, but it seems the table name is being injected into the input name like Create<table>Input, so if that's the case, you can name your table Post instead of post.
input CreatepostsInput {
}
Hi a Real Rails Rookie here. I am trying to write a basic customer mgmt system and when I create a new customer (customer table) I need it to also create 10 sub-records in another table (customer_ownership) with certain predetermined information which will then be updated/modified when we speak to the customer.
I am really struggling with this, do I try and call the sub_record create controller from the create customer controller or do I write a new controller action in the Customer Controller.
Thanks in advance
I think what you want to do is use an active record callback to perform the work you need done thst is associated with data creation.
Or use a service object design pattern to Perform all actions.
Or you can just add the code for the task to be done after create as a method and call the method directly instead of calling it with a callback.
Or this functionality could live on the model. All oth these options could be considered the “Rails Way” depending on who you talk to.
My preferred method would be...
In controllers/my_object_contoller.rb
def create
#object = MyObject.create(my_object_params)
my_private_method
end
private
def my_private_method
# create auxiliary data objects here
end
Also look into ActiveRecord associations
Because there are ways to create two data models that are programmatically linked or associated with one another using foreign_key ids on the DB columns.
Rails offers an excellent api for you to use which I’ve linked to the rails guide for above.
Such an implementation using active record associations might look like this...
# 'app/models/user.rb
class User < ActiveRecord::Base
has_one :address
...
end
# 'app/models/address.rb'
class Address < ActiveRecord::Base
belongs_to :user
...
end
# 'db/migrate/<timestamp>_create_users.rb'
class CreateUsers < ActiveRecord::Migration[5.2]
def change
create_table :users do |t|
t.string :email
t.string :first_name
t.string :last_name
t.timestamps
end
end
end
# 'db/migrate/<timestamp>_create_addresses.rb'
class CreateAddresses < ActiveRecord::Migration[5.2]
def change
create_table :addresses do |t|
t.references :user, null: false, foreign_key: true
t.string :house_number, null: false
t.string :streen_name
t.string :city
t.string :state
t.string :zip_code
t.timestamps
end
end
end
This give us a new way to manipulate our data. If we create a new user and the extra data we want to add is the users address. Then we can just collect all this data in a form and then user the methods that including the has_one helper gives us. For example...
def create
#user = User.new(params[:user])
#address = Address.new(params[:address])
#user.address = #address
#user.save
end
Of course this is all pseudo code so you should really dive into to active record association link I placed above
I've got a Rails app where Users are able to keep track of airing shows and episodes.
To simplify the process of keeping track of (not yet) watched shows, users are able to synchronize their account with other services. They can, in their user settings page, choose which service they want to synchronize with.
To synchronize, I load their profile from the other service, and then run it through an algorithm which detects changes from the last synchronization, and updates the DB accordingly. In order to store the last synchronization status, for each Show ID, I create a "UsersSyncIdStatus" object which stores the show ID, as well as the current status for that show in the synchronized service.
Note that the services do not use the same Show IDs as my website, which means that I have a table which I can use to "convert" from their show IDs to my show IDs. Since the information each service provides is different, they must be stored in different tables.
Right now, this is (a simplified version of) how the DB schema is set up:
create_table "service1_ids", force: :cascade do |t|
t.integer "service_id", null: false
t.integer "show_id", null: false
[...]
end
create_table "service2_ids", force: :cascade do |t|
t.integer "service_id", null: false
t.integer "show_id", null: false
[...]
end
create_table "users_sync_id_statuses", force: :cascade do |t|
t.integer "user_id"
t.integer "service_id", null: false
t.integer "sync_status", default: 0, null: false
t.datetime "sync_date", null: false
[...]
end
create_table "users", force: :cascade do |t|
[...]
t.datetime "synced_at"
t.boolean "sync_enabled", default: false, null: false
t.integer "sync_method", default: 0, null: false
[...]
end
In particular, users.sync_method is an enum which stores the service the user has selected for synchronization:
SYNC_METHODS = {
0 => {
symbol: :service1,
name: 'Service1',
model_name: 'Service1Id',
show_scope: :service1_ids
}
1 => {
symbol: :service2,
name: 'Service2',
model_name: 'Service2Id',
show_scope: :service2_ids
}
}
This means I can easily know the model name of the IDs of a specific user by just doing SyncHelper::SYNC_METHODS[current_user.sync_method][:model_name].
Now, the question is, how can I have a relationship between "users_sync_id_statuses" and "serviceN_ids"? In order to know which class the "service_id" column corresponds to, I have to 'ask' the user model.
I currently have it implemented as a method:
class User
def sync_method_hash
SyncHelper::SYNC_METHODS[self.sync_method]
end
def sync_method_model
self.sync_method_hash[:model_name].constantize
end
end
class UsersSyncIdStatus
def service_id_obj
self.user.sync_method_model.where(service_id: self.service_id).first
end
end
However, UsersSyncIdStatus.service_id_obj is a method, not a relationship, which means I cannot do all the fancy stuff a relationship allows. For example, I cannot easily grab the UsersSyncIdStatus for a specific user and show ID:
current_user.sync_id_statuses.where(service_id_obj: {show_id: 123}).first
I could turn it into a polymorphic relationship, but I really don't want to have to add a text column to contain the class name, when it is a "constant" from the point of view of each user (for a user to switch synchronization service, all UsersSyncIdStatuses for that user are destroyed, so a user never has more than 1 service type in their UsersSyncIdStatuses).
Any ideas? Thank you in advance!
I don't think vanilla Rails 5 supports what I want to do, someone please correct me if I'm wrong.
Still, after some research into how Rails implements polymorphic relationships, I was able to relatively easily monkey-patch Rails 5 to add this functionality:
config/initializers/belongs_to_polymorphic_type_send.rb:
# Modified from: rails/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb
module ActiveRecord
# = Active Record Belongs To Polymorphic Association
module Associations
class BelongsToPolymorphicAssociation < BelongsToAssociation #:nodoc:
def klass
type = owner.send(reflection.foreign_type)
type.presence && (type.respond_to?(:constantize) ? type.constantize : type)
end
end
end
end
app/models/users_sync_id_status.rb:
class UsersSyncIdStatus
belongs_to :service_id_obj, polymorphic: true, foreign_key: :service_id, primary_key: :service_id
def service_id_obj_type
self.user.sync_method_model
end
end
With this monkey-patch, belongs_to polymorphic associations do not assume that the type field is a varchar column, but instead call it as a method on the object. This means you can very easily add your own dynamic behavior, without breaking any old behavior (AFAIK, didn't do intensive testing on that).
For my specific use-case, I have the sync_id_obj_type method query the user object for the class that should be used in the polymorphic association.
I have a user table and an activity table. The user has many activities. This is what i have in my users table:
class SorceryCore < ActiveRecord::Migration
def change
create_table :users do |t|
t.string :first_name
t.string :surname
t.integer :previous_award
t.integer :chosen_award
t.string :email, :null => false
t.string :crypted_password
t.string :salt
t.timestamps
end
add_index :users, :email, unique: true
end
This is what I have in my activities table:
class CreateActivities < ActiveRecord::Migration
def change
create_table :activities do |t|
t.integer :activity_type
t.string :activity_name
t.string :approver_email
t.references :users, index: true, foreign_key: true
t.timestamps null: false
end
end
In my view I want to show the activity_name, where user_id = the current user's id, and where the the activity_type = 1. I'm not sure where to write this method or how to call it either. I have read through the following link but can't seem to get anything working. http://guides.rubyonrails.org/active_record_querying.html
I think I need to use something along the lines of this:
Activity.where("activity_type <= ?", 1).where("user_id <= ?", current_user.id)
But I'm not sure if this is supposed to go into a method in the controller or the model, and I'm not sure which controller or model it's supposed to go into
In the User model:
# user.rb
def activity_by_type(type = 1)
self.activities.where(activity_type: type).first
end
and then, you can call current_user.activity_by_type(<specify the type here>)
You can use the above method to get any of the activity type for the specified user, by specifying the type in the method call.
One advice I'll give though is to try and use the concept of enums to categorize your activity_type column in the activities table. An example on how, can be found here.
You simply have to query on the current_user.activities association:
#activities = current_user.activites.where(activity_type: "1")
You could also use a scope (which is what SunnyK recommended):
#app/models/user.rb
class User < ActiveRecord::Base
has_many :activities
scope :by_type, ->(type = 1) { activities.where(activity_type: type) }
end
--
If you only wanted to return a single record, you'd have to replace .where with .find_by:
#activity = current_user.activities.find_by activity_type: 1
--
Since you're using an enum, you may wish to use the built-in class method that you'd be able to call:
enum activity_type: [:sports, :photography]
#activity = current_user.activities.sports
Two Models: An Owner and a Dog:
owner.rb
class Owner < ActiveRecord::Base
has_one :dog
end
dog.rb
class Dog < ActiveRecord::Base
belongs_to :owner
end
And here is the schema:
schema.rb
ActiveRecord::Schema.define(version: 123) do
create_table "dogs", force: true do |t|
t.string "name"
t.integer "energy"
t.integer "owner_id"
t.datetime "created_at"
t.datetime "updated_at"
end
add_index "dogs", ["owner_id"], name: "index_dogs_on_owner_id"
create_table "owners", force: true do |t|
t.string "name"
t.string "energy"
t.datetime "created_at"
t.datetime "updated_at"
end
end
Pretty simple setup.
I want an owner to take his dog for a walk. When the walk ends, the owner's energy will drop by 5, AND the dog's energy will drop by 20.
Clearly this walk_the_dog action/method, wherever it is going to live, is effecting two objects: an owner object and a dog object (and of course this dog object happens to be associated to this owner).
I don't know where to put this code. I know I could simply create an action within the owners_controller.rb, but that seems like a bad idea. It would look something like this:
owners_controller.rb
class OwnersController < ApplicationController
def walk_the_dog
#owner = Owner.find(params[:id])
#owner.energy -= 5
#owner.dog.energy -= 20 # this line in particular seems like bad OO design
#owner.save
#owner.dog.save
end
...
end
As I understand it, objects should only change state for themselves and shouldn't change the state of other objects. So this appears to be a bad idea because within the owner controller we are changing the state of not just the owner object, but the associated dog object as well.
I have read about services. It seems like walk_the_dog is an excellent case for a service, because services, as I understand it, allow interactions between objects and state changes for multiple objects. I just don't know how to do it/implement it.
Should there be a service object called walk_the_dog? Should their just be a file within a services directory with a bunch of service methods -- one of which is called walk_the_dog and the owners_controller.rb simply utilizes this method in it's controller? I don't know what the next step is.
Note: I can see someone saying "who cares if this breaks OO design. Just do it and if it works, it works." Unfortunately this is not an option. The application I am working on now followed that mindset. The application grew very large, and now maintaining it is very difficult. I want to get this situation down for the major redesign of the app.
Here are the few things that I would do if I were to refactor this code:
Writing numbers in your code is a bad thing, either you have defined them as constants like ENERGY_PER_WALK_FOR_DOG = 20 or a better way is to define a field in the table of Dog model. This way, it will be much better to manage and assign those values.
add_column :dogs, energy_per_walk, :integer, default: 20
add_column :owners, energy_per_walk, :integer, default: 5
I'd create a method in ApplicationController class:
def walk(resources = [])
resources.each do |resource|
resource.lose_walk_energy # you can refine it more!
end
end
In the folder, app/models/concerns, I would write the following module:
module Walkable
extend ActiveSupport::Concern
# subtract energy_per_walk form the energy saved in db
def lose_walk_energy
self.energy -= self.energy_per_walk
save
end
end
And now, your method reduces to the following method:
def walk_the_dog
#owner = Owner.find(params[:id])
walk([#owner, #owner.dog])
end
I would say that this should be a method in the Owner model. You also need to do both operations in one transaction, to ensure, that both models have been updated.
class Owner
has_one :dog
def walk_the_dog
return false if dog.nil?
transaction do
decrement!(:energy, 5)
dog.decrement!(:energy, 20)
end
end
end