Skip to content

Lesson: Using Rails Nested Attributes behavior to modify Nested Nodes

scherztc edited this page Mar 4, 2014 · 10 revisions

Explanation

Rails has a particular pattern for updating objects across associations. For example, if I have a web form for editing a Member object and the form allows me to create/update Questions and Answers belonging to that Member, I can update the Member and any/all of its Questions and Answers with a single Hash (ie. a single set of POST parameters). In Rails, this is called "nested attributes" behavior. If you read the existing documentation for nested attributes in ActiveRecord, you will find that our RDF Nodes behave in similar ways.

Steps

Step: Set up the RDF Types

Follow the steps in Lesson: Define a Complex Network of Related RDF Types to set up the Member, Question and Answer types.

Step: Add Nested Attributes behaviors to your Classes

In order to use nested attributes behaviors, we need to tell our classes to support it. This requires one line in each of the classes that will support it.

In member.rb add accepts_nested_attributes_for :questions and in question.rb add accepts_nested_attributes_for :answers

Example member.rb:

require "active-fedora"
require "./vocabularies/questions_vocab"
require  "./question.rb"
class Member
  include ActiveFedora::RdfObject
  rdf_type "http://xmlns.com/foaf/0.1/Person"
  map_predicates do |map|
    map.nick(in: RDF::FOAF)
    map.givenName(in: RDF::FOAF)
    map.familyName(in: RDF::FOAF)
    map.questions(to: "askedQuestion", in: QuestionsVocab, class_name: "Question")
  end
  accepts_nested_attributes_for :questions
end

Example question.rb:

require "active-fedora"
require "./vocabularies/questions_vocab"
require  "./answer.rb"
class Question
  include ActiveFedora::RdfObject
  rdf_type QuestionsVocab.Question
  map_predicates do |map|
    map.title(in: RDF::DC)
    map.description(in: RDF::DC)
    map.answers(to: "hasAnswer", in: QuestionsVocab, class_name: "Answer")
  end
  accepts_nested_attributes_for :answers
end

Step: Build a Graph from a Hash using .attributes=

The real utitlity of nestes attributes behavior is that you can pass whole Hashes of attributes and sub-nodes into a Node.

For example, here's a Hash that we can use to create a Member with one Question that has 2 answers.

{
  member: {
    nick: "thenick",
    givenName: "Julius",
    familyName: "Caesar",
    questions_attributes: [{
      title: "Where are we?",
      description: "I have no idea how we got here.",
      answers_attributes: [
        {description: "Don't sweat it. I don't know either."},
        {description: "The answer is 42."}
      ]
    }]
  }
}

In the previous step, we told Member nodes to accept nested attributes for questions and told Question nodes to accept nested attributes for answers. Because we set that up, now when we put question_attributes into the Hash of attributes for updating a Member or put answers_attributes in the Hash of attributes for updating a Question the info will be used to build/update Question and Answer nodes.

In the console, pass the

require "./member.rb"
member = Member.new(RDF::Graph.new)
params = {
  member: {
    nick: "thenick",
    givenName: "Julius",
    familyName: "Caesar",
    questions_attributes: [{
      title: "Where are we?",
      description: "I have no idea how we got here.",
      answers_attributes: [
        {description: "Don't sweat it. I don't know either."},
        {description: "The answer is 42."}
      ]
    }]
  }
}
member.attributes = params[:member]
puts member.graph.dump(:ntriples)

Step: Build multiple nodes in a graph by passing an Array of Hashes into .attributes=

In the previous step, the value of questions_attributes was a Hash and answers_attributes was an Array. This highlights the fact that you can either pass attributes to a single associated node by using a single Hash in one of your _attributes keys, or you can pass attributes to multiple associated nodes by using an Array of Hashes in one of the _attributes keys.

In the console

require "./member.rb"
member = Member.new(RDF::Graph.new)
member.attributes = {questions_attributes: [{title: "What's my question?"}]}
puts member.graph.dump(:ntriples)
_:g70249387018280 <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://xmlns.com/foaf/0.1/Person> .
_:g70249387018280 <http://example.com/ontologies/QuestionsAndAnswers/0.1/askedQuestion> _:g70249388263360 .
_:g70249388263360 <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://example.com/ontologies/QuestionsAndAnswers/0.1/Question> .
_:g70249388263360 <http://www.w3.org/1999/02/22-rdf-syntax-ns#value> "What's my question?" .
 => nil 

Notice that it built a Question node for you and added the text we provided into that node. It uses the rdf:value predicate to put your string into the graph. In the next step, we will look at how to customize where the value is written.

Below are some other examples of how you can use this feature to add nodes to the graph.

... you can use Hashes of values ...

params = {
  member: {
  nick: "thenick",
  question_attributes: "What's my question?"
  }
}
member.attributes = params[:member]

You can also mix & match.

params = {
  member: {
  nick: "thenick",
  question_attributes: {
    title: "What's my question?",
    answer_attributes: "It doesn't matter."
  }
 }
}
member.attributes = params[:member]

Step: Change the default write point for values passed into nested attributes

As you saw in the previous step, when you pass a String value into a nested attribute, the code is smart enough to know that it should build a node for you. By default, it will use the rdf:value predicate to assert your String within the node it created. You can change this by overriding default_write_point_for_values on the class.

In question.rb, add this method definition to the Question class. Make sure it's outside of the map_predicates block, .

def default_write_point_for_values
  [:title]
end

Now create a Question on the console using the nested attributes behavior.

require "./member.rb"
member = Member.new(RDF::Graph.new)
member.attributes = {questions_attributes: [{title: "What's my question?"}]}
member.questions.first.title
# => ["What's my question?"]
puts member.graph.dump(:ntriples)
_:g70118938931980 <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://xmlns.com/foaf/0.1/Person> .
_:g70118938931980 <http://example.com/ontologies/QuestionsAndAnswers/0.1/askedQuestion> _:g70118936714080 .
_:g70118936714080 <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://example.com/ontologies/QuestionsAndAnswers/0.1/Question> .
_:g70118936714080 <http://purl.org/dc/terms/title> "What's my question?" .
 => nil 

Because we specified default_write_point_for_values to use the the mapped :title predicate, the automatically-built Question node uses the dc:title predicate to assert the String value we provided.

You can also set the default_write_point_for_values to be multiple nodes deep.

For the sake of the example, let's say that you want Member nodes to get auto-built with a Question & Answer, putting your string into the answer's description. To do this, add these lines to member.rb:

def default_write_point_for_values
  [:questions, :answers, :description]
end

Now in the console, create a Member node and set its attributes with a String

require "./member.rb"
member = Member.new(RDF::Graph.new)
member.attributes = "The Provided Value"
member.questions.first.answers.first.description
# => ["The Provided Value"]
puts member.graph.dump(:ntriples)
_:g70350722556420 <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://xmlns.com/foaf/0.1/Person> .
_:g70350722556420 <http://example.com/ontologies/QuestionsAndAnswers/0.1/askedQuestion> _:g70350710431760 .
_:g70350710431760 <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://example.com/ontologies/QuestionsAndAnswers/0.1/Question> .
_:g70350710431760 <http://example.com/ontologies/QuestionsAndAnswers/0.1/hasAnswer> _:g70350722348680 .
_:g70350722348680 <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://example.com/ontologies/QuestionsAndAnswers/0.1/Answer> .
_:g70350722348680 <http://purl.org/dc/terms/description> "The Provided Value" .

Next Step

Go on to Lesson: Use Rails fields_for helper to Create Forms that Edit Complex Nested RDF Graphs or return to the Tame your RDF Metadata with ActiveFedora landing page.

Clone this wiki locally