-
Notifications
You must be signed in to change notification settings - Fork 63
Lesson: Using Rails Nested Attributes behavior to modify Nested Nodes (AF7)
#Not yet fully updated for AF7
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 Resources behave in similar ways.
Follow the steps in Lesson: Define a Complex Network of Related RDF Types (AF7) to set up the Member, Question and Answer types.
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 "./lib/rdf_vocabularies/questions_vocab.rb"
require "./lib/rdf_types/question.rb"
class Member < ActiveFedora::Rdf::Resource
configure type: RDF::FOAF.Person
property :nick, predicate: RDF::FOAF.nick
property :givenName, predicate: RDF::FOAF.givenName
property :familyName, predicate: RDF::FOAF.familyName
property :questions, predicate: QuestionsVocab.askedQuestion, class_name: "Question"
accepts_nested_attributes_for :questions
end
Example question.rb
:
require "active-fedora"
require "./lib/rdf_vocabularies/questions_vocab"
require "./lib/rdf_types/answer.rb"
class Question < ActiveFedora::Rdf::Resource
configure type: QuestionsVocab.Question
property :title, predicate: RDF::DC.title
property :description, predicate: RDF::DC.description
property :answers, predicate: QuestionsVocab.hasAnswer, class_name: "Answer"
accepts_nested_attributes_for :answers
end
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: { '1' => {
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 attributes to a Member object.
require "./lib/rdf_types/answer"
require "./lib/rdf_types/question"
require "./lib/rdf_types/member"
member = Member.new
params = {
member: {
nick: "thenick",
givenName: "Julius",
familyName: "Caesar",
questions_attributes: { '1' => {
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.dump(:ntriples)
_:g70241353077080 <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://xmlns.com/foaf/0.1/Person> .
_:g70241353077080 <http://xmlns.com/foaf/0.1/nick> "thenick" .
_:g70241353077080 <http://xmlns.com/foaf/0.1/givenName> "Julius" .
_:g70241353077080 <http://xmlns.com/foaf/0.1/familyName> "Caesar" .
_:g70241353077080 <http://example.com/ontologies/QuestionsAndAnswers/0.1/askedQuestion> _:g70241341460400 .
_:g70241341859940 <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://example.com/ontologies/QuestionsAndAnswers/0.1/Answer> .
_:g70241341859940 <http://purl.org/dc/terms/description> "Don't sweat it. I don't know either." .
_:g70241351081120 <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://example.com/ontologies/QuestionsAndAnswers/0.1/Answer> .
_:g70241351081120 <http://purl.org/dc/terms/description> "The answer is 42." .
_:g70241341460400 <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://example.com/ontologies/QuestionsAndAnswers/0.1/Question> .
_:g70241341460400 <http://purl.org/dc/terms/title> "Where are we?" .
_:g70241341460400 <http://purl.org/dc/terms/description> "I have no idea how we got here." .
_:g70241341460400 <http://example.com/ontologies/QuestionsAndAnswers/0.1/hasAnswer> _:g70241341859940 .
_:g70241341460400 <http://example.com/ontologies/QuestionsAndAnswers/0.1/hasAnswer> _:g70241351081120 .
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.
Now try:
require "./lib/rdf_types/member.rb"
member = Member.new
member.attributes = {questions_attributes: [{title: "What's my question?"}]}
puts member.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", questions_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]
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.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.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" .
Go on to Lesson: Use Rails fields_for helper to Create Forms that Edit Complex Nested RDF Graphs (AF7) or return to the Tame your RDF Metadata with ActiveFedora landing page.