In database terminology primary key refers to the column in a table that's intended to be the primary way of identifying rows. Each table must have exactly one, and it needs to be unique. This is usually some kind of a unique identifier associated with objects presented by the table, or if such an identifier doesn't exist simply a running ID number (which is incremented automatically).
Extra Chapter: Managing Relations¶
There is a topic that comes up in many projects but was not covered in the MusicMeta API example: managing relationships between existing
resources
. The scope of the API only included resources that have static relations with each other - i.e. relationships that do not change over time. We can however easily come up with another set of data for the API that does need assignment of existing resources to relationships with each other: the members of bands change over time. This is also a many-to-many relationship because there's no rule against being in multiple bands. Specification¶
In order to accomplish this, we need to introduce a new
model
to our database: Musician. Also, because musician's relationship with artist is many-to-many, a relationship table is required. These are shown below. artist_membership_table = db.Table("membership",
db.Column("musician_id", db.Integer, db.ForeignKey("musician.id"), primary_key=True),
db.Column("artist_id", db.Integer, db.ForeignKey("artist.id"), primary_key=True)
)
class Musician(db.Model):
id = db.Column(db.Integer, primary_key=True)
unique_name = db.Column(db.String, nullable=False, unique=True)
first_name = db.Column(db.String)
last_name = db.Column(db.String, nullable=False)
artists = db.relationship("Artist", secondary=artist_membership_table)
def __repr__(self):
return "{}, {} <{}>".format(self.last_name, self.first_name, self.id)
We would also add the following relationship attribute to the Artist model:
members = db.relationship("Musician", secondary=artist_membership_table)
Managing memberships on the code level is quite straightforward: the relationships work just like Python lists so we can append and remove members from the artist end rather effortlessly. The real question is how this should be managed through a RESTful API. We don't really want to do this with a PUT request although it would be possible to have an array of musician unique names as an attribute in a
JSON
request body
. This is awkward because we will have to check through the entire submitted list for differences, or delete all of the existing relationships and then create everything anew. This is not a significant performance hit in our case because the number of musicians per artist is relatively small. However, we have better ways to do it.Solution 1: Relationship Resources¶
A resource-oriented solution is to represent the connection between two resources as a resource itself. This is a special case of a resource that doesn't contain any data but it is instead meaningful just because it exists. This relationship resource can be routed to e.g.
/api/artists/{artist_unique_name}/members/{musician_unique_name}/
The existence of a resource fitting this
URL template
means that the musician identified by their unique name is a member in the artist identified by its unique name. But how should this type of resource be created? There are two options: - by POST request to
/api/artists/{artist_unique_name}/members/
- by PUT request to
/api/artists/{artist_unique_name}/members/{musician_unique_name}/
So far we've been emphasizing the creation of new resources with POST. However, creating resources with PUT is also valid. It's simply not usually recommended because in order to make a PUT request, the client needs to know the
URI
of the resource beforehand. For example in our case, if the client was tryign to add a second artist with a name that's already taken, its best guess might end up replacing the existing artist's information with the new one's. The creation of a relationship resource is different: the URI is formed of existing unique names so the client actually does have a way of knowing what the URI should be. Since you should already know how to do this with POST, we're going to show how to use PUT.As usual we need to inform the client about this action by adding a
hypermedia
control. This can be in the artist resource, or in the members resource should we make one. The control uses an href template with an attached schema
that tells how to form the final URI. Much like the album sorting control we did, except this time the URL variable is in the address itself, not in the query string
. {
"mumeta:add-member": {
"href": "/api/artists/scandal/members/{musician}/",
"title": "Add a member to this artist",
"isHrefTemplate": true,
"method": "PUT",
"schema": {
"properties": {
"musician": {
"type": "string",
"description": "Musician's unique name"
}
}
}
}
}
Removing a member becomes a very simple interaction: simply send DELETE to the resource that represents the relationship. For our resource representation the following control can be attached to each member.
{
"mumeta:remove-member": {
"href": "/api/artists/scandal/members/ogawa_tomomi/",
"title": "Remove this member from the artist",
"method": "DELETE"
}
}
There is actually no reason for the relationship resource to support GET - there's no data there so there's very little reason for clients to retrieve it.
Solution 2: PATCH Method¶
Alternatively the problem can be solved using the HTTP PATCH method. This method has not been introduced earlier because it is not as widely used. Mostly because its definition is more vague - a client developer cannot infer what a PATCH method is supposed to do like they can with PUT. The PATCH method is used to send a representation of changes to the resource. The correct way of representing changes depends on the API which greatly reduces the uniformity of this
HTTP method
. Please note that PATCH should not be treated as a partial PUT. If all you are doing is replacing some attribute values, you should always use PUT because it doesn't need any explanation, and because it is idempotent
. Filling the unchanged values into the request is not a big deal for clients.What PATCH should be used for is to describe changes that would be inconvenient to implement with PUT. Like assigning new members to a group. However, the easiest way to illustrate the use of PATCH and how it differs from PUT is incrementing a counter. Let's assume a resource that has an integer attribute simply called
"counter"
. A PATCH request body
to increment a counter by one (without knowing its current value) could be:{
"operation": "increment",
"attribute": "counter"
}
This is a format we just literally cooked up so it is definitely not very universal - and this is the problem with PATCH. There is a standard propsal for PATCH documents called JSON Patch which should help us a little bit. The JSON Patch version of the same operation is not quite as illustrative, but should be pretty clear:
{
"op": "add",
"path": "/counter",
"value": 1
}
The reverse operation is "remove". We can use this format to represent addition and removal of members, e.g. to add a member:
{
"op": "add",
"path": "/members",
"value": "ogawa_tomomi"
}
and to remove:
{
"op": "remove",
"path": "/members",
"value": "ogawa_tomomi"
}
In terms of
hypermedia
controls we can combine the two into one control. This is the description that should be dropped into the artist resource or the members resource (if one is added - the example uses the artist resource). {
"mumeta:manage-members": {
"href": "/api/artists/scandal/",
"title": "Add or remove members from this artist",
"method": "PATCH",
"schema": {
"type": "object",
"properties": {
"op": {
"description": "Add or remove members"
"type": "string",
"enum": ["add", "remove"]
},
"path":{
"description": "Affected attribute",
"type": "string",
"default": "/members",
"enum": ["/members"]
},
"value": {
"description": "Unique name of the musician to add/remove",
"type": "string"
}
}
}
}
}
Note that although the path attribute has only one possible value the client still has to send it since we should follow the JSON Patch standard in our validation. It would also be possible to describe two different controls where the op attribute is also fixed to separate the adding and removing of members.
Relationships with Data¶
Actually there is one more thing to discuss about this topic. The examples above are for a simple case where the relationship between artist and musician simply exists or doesn't. However for a real use case it makes more sense to include time spans because sometimes members come and go. In this case it definitely makes more sense to turn the relationship itself into a
resource
. Changes to Models¶
When the relationship itself contains data this also means that in the backend side we should turn the relationship table into a proper
model
.class ArtistMembership(db.Model):
__tablename__ = "membership"
musician_id = db.Column(db.Integer, db.ForeignKey("musician.id", ondelete="CASCADE"), primary_key=True)
artist_id = db.Column(db.Integer, db.ForeignKey("artist.id", ondelete="CASCADE"), primary_key=True)
joined = db.Column(db.Date, nullable=False)
left = db.Column(db.Date, nullable=True)
musician = db.relationship("Musician", back_populates="artists")
artist = db.relationship("Artist", back_populates="members")
We've set the __tablename__ attribute this time because we want the name to match the table that we created previously instead of using the class name in lowercase. After changing the association table, the Artist and Musician models no longer refer to each other directly with the relationship's secondary attribute. Instead they both have a relationship to the association model but no foreign key to it. So for change the relationship attributes
# for Musician model
artists = db.relationship("ArtistMembership", back_populates="artist", cascade="all,delete-oprhan")
# for Artist model
members = db.relationship("ArtistMembership", back_populates="musician", cascade="all,delete-oprhan")
Now in order to add a member to an artist we have to create the membership object in addition to the musician which adds an extra step:
In [1]: from app import db, Musician, ArtistMembership, Artist
In [2]: import datetime
In [3]: member = Musician(unique_name="ogawa_tomomi", first_name="Tomomi", last_name="Ogawa")
In [4]: artist = Artist.query.filter_by(unique_name="scandal").first()
In [5]: membership = ArtistMembership(joined=artist.formed, musician=member)
In [6]: artist.members.append(membership)
In [7]: db.session.commit()
And in order to access a member through the relationship:
In [8]: artist.members[0].musician.first_name
Out[8]: "Tomomi"
Changes to Resources¶
This time it's definitely correct to treat memberships as
resources
. Whether it should support GET or not is up to preference. Overall it will mostly the same as the first solution we had to the previous problem. However since the resource now contains data, PUT could do two things: create a new one or edit an existing one (e.g. to update the membership with a left attribute when someone leaves). Another problem with using PUT in this case is that for the first time Mason lets us down a bit. In Mason, the schema attribute of a control is used to describe both request body
and the parameters for a URL template
. The problem is we would need both, but there is no way to indicate what part of the schema describes the request body and what are parameters. Sure, clients can parse it from the href attribute but the schema itself cannot be used to validate the request body if it also contains parameters. So this time the better route is to make the membership list a resource (at
/api/artists/{artist}/members/
) that supports GET and POST, where the POST method is used for adding new members. Removal of members is still handled by DELETE to the membership resource like previously. We're not going to show the entire membership list resource, just the control that adds a new member. {
"mumeta:add-member": {
"href": "/api/artists/{artist}/members/",
"title": "Add a new member to this artist.",
"method": "POST",
"schema": {
"type": "object",
"properties": {
"first_name": {
"type": "string",
"description": "The members's first name"
},
"last_name": {
"type": "string",
"description": "The members's last name"
},
"joined": {
"description": "Date joined",
"type": "string",
"pattern": "^[0-9]{4}-[01][0-9]-[0-3][0-9]$",
"format": "date"
},
"left": {
"description": "Date left",
"type": "string",
"pattern": "^[0-9]{4}-[01][0-9]-[0-3][0-9]$",
"format": "date"
},
},
"required": ["last_name", "joined"]
}
}
}
Anna palautetta
Kommentteja materiaalista?