mmmmmm
mmmmmm

Reputation: 63

Rails: How do I save an object to a model after calling Model.new?

I have a User model and a Message Model. User has_many messages and Message belongs_to user.

In one method of the users_controller, I get email data from an API, initialize a new Message with that data (using thisMessage = Message.new(messageId, senderName...)). In the controller I do this multiple times, and store these messages in an Array called listOfMessages. So far, so good, I can use attributes of a message by doing listOfMessages[0].sender (sender being a Message model attribute) for example.

But, these messages are not saved when I refresh the page of course. If I try to save the messages to the database using 'thisMessage.save' after each time I initialize a new one, I get the error "undefined method '[]' for nil:NilClass".

I have also tried 'thisMessage.save!', and I get the error "undefined method 'has_key?' for nil:NilClass"

So how should I save these messages?

Message Model

class Message < ActiveRecord::Base
belongs_to :user

def initialize(inputId, inputSender, inputSenderRealName, inputRecievedAt, inputSubject, inputContent, inputIsRead, inputIsReplied, inputImportanceLevel)
    @emailId = inputId
    @sender = inputSender
    @senderRealName = inputSenderRealName
    @recievedAt = inputRecievedAt
    @subject = inputSubject
    @content = inputContent
    @isRead = inputIsRead
    @isReplied = inputIsReplied
    @importanceLevel = inputImportanceLevel
end

The Method from users_controller I am using

def messageList
    @user = current_user
    if @user.current_message == nil
        @user.current_message = 0
    else

    end



    puts "Need Loading " + @user.needsLoading.to_s
    @listOfMessages = Array.new
    messageIdList = Array.new

    --API stuff removed--

        body1 = singleMessage['bodies'][0]['content']
        senderName = singleMessage['addresses']['from'][0]['email']
        senderActualName = singleMessage['addresses']['from'][0]['name']
        recieveTime = singleMessage['sent_at']
        subjectText = singleMessage['subject']
        isRead = singleMessage['flags']['seen']
        hasReplied =  singleMessage['flags']['answered']


        thisMessage = Message.new(messageId, senderName, senderActualName, recieveTime, subjectText, body1, isRead, hasReplied, 0)

        @listOfMessages << thisMessage
        thisMessage.save
    }

    puts @user.messages.length
    @responseText = response
    puts @user.needsLoading.to_s
    puts @user.current_message
    @user.update_column(:needsLoading, 1)



end

The end goal is to be able to do things like "current_user.messages[1].sender" somewhere else. Thanks!

Upvotes: 2

Views: 8554

Answers (1)

max
max

Reputation: 101811

1. Don't override the initialize method on ActiveRecord::Model.

ActiveRecord models already take a hash of attributes:

User.new(name: 'Max', awesome: true)

If the attribute exists it will be set on the model instance. The initializer does a lot of work for and you should not be clobbering it lightly.

If you really really need to do something in the initializer make sure you call super and keep the interface the same as one would expect for a model.

def initalize(hash = {}, &block)
  super
  # do something custom here
end

But in most cases you can use callbacks or custom attribute setters instead.

2. Follow the ruby conventions.

Ruby and Rails for that matter has pretty simple and strong conventions for naming:

  • CONSTANTS are all caps. Ruby enforces this.
  • ModuleName and ClassName are CamelCase (and also a kind of constant).
  • variable_name and attribute_name are snake_case. Ruby does not care - but the community does. Disregard this and you'll never be able to sit with the cool kids.

3. .update and .assign_attributes

There are a few ways to change a model instance but it's important to note the difference between changing the model instance in memory and committing the changes to the database.

@order = Order.new

# change a single attribute.
@order.total = 999 

# Update multiple attributes at once
@order.assign_attributes(
  total: 669,
  shipping_method: 'FEDEX'
) 

# So far we have only updated the instance in memory.
@order.save # Commit the changes to the database

# We can also update the database straight away.

@order.update_attribute(:total, 999) # one attribute at a time

# or with a hash.
@order.update(
  total: 669,
  shipping_method: 'FEDEX'
) 

# Using related objects works the same:
@order.update(
  customer: @customer
) 

# Note that this will also save the related object to the database.
@order.update(
  customer: Customer.new
) 

4. Use Service Objects to create model instances from API calls.

This limits the amount of contact points between the external api and your application. And they are are really easy to test.

# Import a message from SomeApi
class MessageImportService

  def initialize(client = MyApi)
    @client = client
  end
  
  # @return [Array]
  def call
    data = @client.call_some_api_method || []
    data.map do |raw_msg|
      Message.new(
        body1 : raw_msg['bodies'][0]['content'],
        sender_name : raw_msg['addresses']['from'][0]['email'],
        sender_actual_aame : raw_msg['addresses']['from'][0]['name'],
      ) 
    end
  end
end

From your controller you would do:

@messages = MessageImportService.new.call

https://blog.engineyard.com/2014/keeping-your-rails-controllers-dry-with-services

Upvotes: 3

Related Questions