4

I'm trying to the has_one and has_many methods in one model. A taskflow can have many tasks but it also has a single default task.

I'm trying to create a column in the taskflow table that contains the id of a task. However when I try and set that default task it does not work.

class Taskflow < ActiveRecord::Base
    has_many :tasks
    has_one :default_task, :class_name => 'Task'

class Task < ActiveRecord::Base
    belongs_to :taskflow

I seed the database then in the rails console I try to assign a task to be a taskflow default_task:

taskflow1 = Taskflow.first
task1 = Task.first
taskflow1.default_task = task1

This doesn't work with the taskflow default_task value remaining 'nil'. What the correct way to achieve the desired behaviour?

Any help would be much appreciated.

Edit

Migration files are:

class CreateTaskflows < ActiveRecord::Migration
  def change
    create_table :taskflows do |t|
      t.string :title
      t.string :description
      t.references :default_task
      t.timestamps null: false
    end
  end
end

class CreateTasks < ActiveRecord::Migration
  def change
    create_table :tasks do |t|
      t.string :task_type
      t.text :help
      t.text :data
      t.belongs_to :taskflow
      t.timestamps null: false
    end
  end
end
RobotEyes
  • 4,929
  • 6
  • 42
  • 57
  • first off it looks like you never set task so "taskflow1.default_task = task" will set the default_task to nil because task is never set. Do all TaskFlows have the same default task or does it change between TaskFlow instances? – ruby_newbie Jun 22 '16 at 15:22
  • Sorry that was a typo. The default task could be different between taskflow instances. Many thanks. – RobotEyes Jun 22 '16 at 15:31

4 Answers4

5

I would implement it differently. I would create a boolean field default_task on the Task model. And Taskflow would have the following has_one and has_many associations:

class Taskflow < ActiveRecord::Base
    has_many :tasks
    has_one :default_task, class_name: 'Task', condition: proc{"tasks.default_task = true"}

Semantically, Taskflow should have many tasks and have one default task among those tasks. And on my opinion that a taskflow belongs to a default task sounds a bit artificial.

You can also add a functionality to constantly maintain a single task as a default task (with bit set to true, more details here), or create a default task at the time of creation of a Taskflow. It all depends on your requirements.

AND, if you still want to have the default_task_id column to be on your Taskflow model and use has_one association, you can do the following:

class Taskflow < ActiveRecord::Base
    has_many :tasks
    has_one :default_task, class_name: "Task", primary_key: "default_task_id", foreign_key: "id"
Community
  • 1
  • 1
2

has_one is used to specifie a one-to-one association with another class. This method should only be used if the other class contains the foreign key.

In your case, you should use belong_to as the default_task reference is in the TaskFlow model (in your migration).

The TaskFlow model has two relations:

  • A TaskFlow has many tasks
  • A TaskFlow has one specific task that is the default task.

class Taskflow < ActiveRecord::Base has_many :tasks belongs_to :default_task, :class_name => 'Task'

class Task < ActiveRecord::Base belongs_to :taskflow

The way you had it, Rails wouldn't know which task is the default one.

dquimper
  • 916
  • 7
  • 7
2

People here are right about belongs_to or how to hack has_one. I understand that belongs_to does not feel right in this situation, but if you are using "referencing" in table you normally always want to use belongs_to. So I would assume to mark the default task on the tasks itself as default - but if they can be part of different task_flows where different tasks could be the default - then of course that's not possible.

Another option to those here already mentioned is: You can add a scope to the has_many :tasks relation. Like this:

has_many :tasks do
  def default
    joins(:task_flow).where('tasks.id = task_flows.default_task_id').first
  end
end

Then you can request for

@task_flow.tasks.default
bpieck
  • 241
  • 1
  • 6
  • I don't see how it is different from a scope (just less clear), but, well, Rails is very good because you can express things in a variety of ways to make them clear and fitting for a particular situation. BTW, about has_one "hack", it is not a hack. `condition` parameter in `has_one` and `has_many` is a well defined and useful behavior for customizing associations. –  Jun 23 '16 at 19:59
  • I call it "hack" because it contradicts conventions. I think you can agree at least, that you have configure much, to make has_one working in the data model here. Edit - PS: I just looked into your current answer. And to mark it via boolean does not feel like a hack. But to reconfigure primary_key and foreign_key to do work nearly the opposite of the convention does feel like a "hack" to me ^^ – bpieck Jul 07 '16 at 05:21
  • 1
    Well, I call a "hack" things that are done with only purpose to make something work even if it does not really reflect desired design. Let's take our current case as an example. RobotEyes used belongs_to for default_task in Taskflow and at the same time Taskflow has_many tasks. It even sounds artificial: a task flow has many task and belongs to a default task. Much better version would be: a taskflow has many tasks and one of them is a default task (I still think that a bool on the task would be a better solution, because it allows you to have more than 1 default task later). –  Jul 07 '16 at 09:27
  • belongs_to was used just because it started working this way, but does it really reflect the design idea? One of the huge pluses of Rails over other frameworks is that it is very expressive, and it allows you to reflect in the code all kinds of modeling. There are pretty strict definitions for "has one", "has many" and such, and if something is supposed to be "has one", it should not be converted to "belongs to" just to make it work. The "primary_key" and "foreign_key" attributes that I used were purposely designed in Rails to reflect such cases and they are well defined Rails conventions. –  Jul 07 '16 at 09:40
1

I got this to work. Instead of has_one, belongs_to is needed in the model.

class Taskflow < ActiveRecord::Base
    has_many :tasks
    belongs_to :default_task, :class_name => 'Task'

class Task < ActiveRecord::Base
    belongs_to :taskflow

I think something to do with has_one having the reverse relationship to my assumption. http://guides.rubyonrails.org/association_basics.html#the-has-one-association

RobotEyes
  • 4,929
  • 6
  • 42
  • 57
  • 2
    By using belongs_to you're implying that a Task has one/many TaskFlows, even if you haven't explicitly stated such an association. Does that accurately reflect your requirements? – hypern Jun 22 '16 at 15:49